Initial commit
diff --git a/includes/admin/class-wc-rest-stripe-account-controller.php b/includes/admin/class-wc-rest-stripe-account-controller.php
new file mode 100644
index 0000000..4efabd0
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-account-controller.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Account_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for retrieving Stripe's account data.
+ *
+ * @since 5.6.0
+ */
+class WC_REST_Stripe_Account_Controller extends WC_Stripe_REST_Base_Controller {
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/account';
+
+	/**
+	 * The account data utility.
+	 *
+	 * @var WC_Stripe_Account
+	 */
+	private $account;
+
+	/**
+	 * Stripe payment gateway.
+	 *
+	 * @var WC_Gateway_Stripe
+	 */
+	private $gateway;
+
+	public function __construct( WC_Gateway_Stripe $gateway, WC_Stripe_Account $account ) {
+		$this->gateway = $gateway;
+		$this->account = $account;
+	}
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_account' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/summary',
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_account_summary' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/webhook-status-message',
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_webhook_status_message' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/refresh',
+			[
+				'methods'             => WP_REST_Server::EDITABLE,
+				'callback'            => [ $this, 'refresh_account' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+	}
+
+	/**
+	 * Retrieve the Stripe account information.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function get_account() {
+		return new WP_REST_Response(
+			[
+				'account'                => $this->account->get_cached_account_data(),
+				'testmode'               => WC_Stripe_Webhook_State::get_testmode(),
+				'webhook_status_message' => WC_Stripe_Webhook_State::get_webhook_status_message(),
+				'webhook_url'            => WC_Stripe_Helper::get_webhook_url(),
+			]
+		);
+	}
+
+	/**
+	 * Return a summary of Stripe account details.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function get_account_summary() {
+		$account = $this->account->get_cached_account_data();
+
+		// Use statement descriptor from settings, falling back to Stripe account statement descriptor if needed.
+		$statement_descriptor = WC_Stripe_Helper::clean_statement_descriptor( $this->gateway->get_option( 'statement_descriptor' ) );
+		if ( empty( $statement_descriptor ) ) {
+			$statement_descriptor = $account['settings']['payments']['statement_descriptor'];
+		}
+		if ( empty( $statement_descriptor ) ) {
+			$statement_descriptor = null;
+		}
+
+		return new WP_REST_Response(
+			[
+				'has_pending_requirements' => $this->account->has_pending_requirements(),
+				'has_overdue_requirements' => $this->account->has_overdue_requirements(),
+				'current_deadline'         => $account['requirements']['current_deadline'] ?? null,
+				'status'                   => $this->account->get_account_status(),
+				'statement_descriptor'     => $statement_descriptor,
+				'store_currencies'         => [
+					'default'   => $account['default_currency'] ?? get_woocommerce_currency(),
+					'supported' => $this->account->get_supported_store_currencies(),
+				],
+				'country'                  => $account['country'] ?? WC()->countries->get_base_country(),
+				'is_live'                  => $account['charges_enabled'] ?? false,
+				'test_mode'                => WC_Stripe_Webhook_State::get_testmode(),
+			]
+		);
+	}
+
+	/**
+	 * Retrieve the webhook status information.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function get_webhook_status_message() {
+		return new WP_REST_Response( WC_Stripe_Webhook_State::get_webhook_status_message() );
+	}
+
+	/**
+	 * Clears the cached account data and returns the updated one.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function refresh_account() {
+		$this->account->clear_cache();
+
+		// calling the same "get" method, so that the data format is the same.
+		return $this->get_account();
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-account-keys-controller.php b/includes/admin/class-wc-rest-stripe-account-keys-controller.php
new file mode 100644
index 0000000..8b6e086
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-account-keys-controller.php
@@ -0,0 +1,232 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Account_Keys_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for saving Stripe's test/live account keys.
+ *
+ * This includes Live Publishable Key, Live Secret Key, Webhook Secret.
+ *
+ * @since 5.6.0
+ */
+class WC_REST_Stripe_Account_Keys_Controller extends WC_Stripe_REST_Base_Controller {
+	const STRIPE_GATEWAY_SETTINGS_OPTION_NAME = 'woocommerce_stripe_settings';
+
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/account_keys';
+
+	/**
+	 * The instance of the Stripe account.
+	 *
+	 * @var WC_Stripe_Account
+	 */
+	private $account;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param WC_Stripe_Account $account The instance of the Stripe account.
+	 */
+	public function __construct( WC_Stripe_Account $account ) {
+		$this->account = $account;
+	}
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_account_keys' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::EDITABLE,
+				'callback'            => [ $this, 'set_account_keys' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'publishable_key'      => [
+						'description'       => __( 'Your Stripe API Publishable key, obtained from your Stripe dashboard.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => [ $this, 'validate_publishable_key' ],
+					],
+					'secret_key'           => [
+						'description'       => __( 'Your Stripe API Secret, obtained from your Stripe dashboard.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => [ $this, 'validate_secret_key' ],
+					],
+					'webhook_secret'       => [
+						'description'       => __( 'Your Stripe webhook endpoint URL, obtained from your Stripe dashboard.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'test_publishable_key' => [
+						'description'       => __( 'Your Stripe testing API Publishable key, obtained from your Stripe dashboard.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => [ $this, 'validate_test_publishable_key' ],
+					],
+					'test_secret_key'      => [
+						'description'       => __( 'Your Stripe testing API Secret, obtained from your Stripe dashboard.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => [ $this, 'validate_test_secret_key' ],
+					],
+					'test_webhook_secret'  => [
+						'description'       => __( 'Your Stripe testing webhook endpoint URL, obtained from your Stripe dashboard.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+				],
+			]
+		);
+	}
+
+	/**
+	 * Retrieve flag status.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function get_account_keys() {
+		$allowed_params  = [ 'publishable_key', 'secret_key', 'webhook_secret', 'test_publishable_key', 'test_secret_key', 'test_webhook_secret' ];
+		$stripe_settings = get_option( self::STRIPE_GATEWAY_SETTINGS_OPTION_NAME, [] );
+		// Filter only the fields we want to return
+		$account_keys = array_intersect_key( $stripe_settings, array_flip( $allowed_params ) );
+
+		return new WP_REST_Response( $account_keys );
+	}
+
+	/**
+	 * Validate stripe publishable keys and secrets. Allow empty string to erase key.
+	 * Also validates against explicit key prefixes based on live/test environment.
+	 *
+	 * @param mixed           $value
+	 * @param WP_REST_Request $request
+	 * @param string          $param
+	 * @param array $validate_options
+	 * @return true|WP_Error
+	 */
+	private function validate_stripe_param( $param, $request, $key, $validate_options ) {
+		if ( empty( $param ) ) {
+			return true;
+		}
+		$result = rest_validate_request_arg( $param, $request, $key );
+		if ( ! empty( $result ) && ! preg_match( $validate_options['regex'], $param ) ) {
+			return new WP_Error( 400, $validate_options['error_message'] );
+		}
+		return true;
+	}
+
+	public function validate_publishable_key( $param, $request, $key ) {
+		return $this->validate_stripe_param(
+			$param,
+			$request,
+			$key,
+			[
+				'regex'         => '/^pk_live_/',
+				'error_message' => __( 'The "Live Publishable Key" should start with "pk_live", enter the correct key.', 'woocommerce-gateway-stripe' ),
+			]
+		);
+	}
+
+	public function validate_secret_key( $param, $request, $key ) {
+		return $this->validate_stripe_param(
+			$param,
+			$request,
+			$key,
+			[
+				'regex'         => '/^[rs]k_live_/',
+				'error_message' => __( 'The "Live Secret Key" should start with "sk_live" or "rk_live", enter the correct key.', 'woocommerce-gateway-stripe' ),
+			]
+		);
+	}
+
+	public function validate_test_publishable_key( $param, $request, $key ) {
+		return $this->validate_stripe_param(
+			$param,
+			$request,
+			$key,
+			[
+				'regex'         => '/^pk_test_/',
+				'error_message' => __( 'The "Test Publishable Key" should start with "pk_test", enter the correct key.', 'woocommerce-gateway-stripe' ),
+			]
+		);
+	}
+
+	public function validate_test_secret_key( $param, $request, $key ) {
+		return $this->validate_stripe_param(
+			$param,
+			$request,
+			$key,
+			[
+				'regex'         => '/^[rs]k_test_/',
+				'error_message' => __( 'The "Test Secret Key" should start with "sk_test" or "rk_test", enter the correct key.', 'woocommerce-gateway-stripe' ),
+			]
+		);
+	}
+
+	/**
+	 * Update the data.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function set_account_keys( WP_REST_Request $request ) {
+		$publishable_key      = $request->get_param( 'publishable_key' );
+		$secret_key           = $request->get_param( 'secret_key' );
+		$webhook_secret       = $request->get_param( 'webhook_secret' );
+		$test_publishable_key = $request->get_param( 'test_publishable_key' );
+		$test_secret_key      = $request->get_param( 'test_secret_key' );
+		$test_webhook_secret  = $request->get_param( 'test_webhook_secret' );
+
+		$settings = get_option( self::STRIPE_GATEWAY_SETTINGS_OPTION_NAME, [] );
+
+		// If all keys were empty, then is a new account; we need to set the test/live mode.
+		$new_account = ! trim( $settings['publishable_key'] )
+					&& ! trim( $settings['secret_key'] )
+					&& ! trim( $settings['test_publishable_key'] )
+					&& ! trim( $settings['test_secret_key'] );
+		// If all new keys are empty, then account is being disconnected. We should disable the payment gateway.
+		$is_deleting_account = ! trim( $publishable_key )
+							&& ! trim( $secret_key )
+							&& ! trim( $test_publishable_key )
+							&& ! trim( $test_secret_key );
+
+		$settings['publishable_key']      = is_null( $publishable_key ) ? $settings['publishable_key'] : $publishable_key;
+		$settings['secret_key']           = is_null( $secret_key ) ? $settings['secret_key'] : $secret_key;
+		$settings['webhook_secret']       = is_null( $webhook_secret ) ? $settings['webhook_secret'] : $webhook_secret;
+		$settings['test_publishable_key'] = is_null( $test_publishable_key ) ? $settings['test_publishable_key'] : $test_publishable_key;
+		$settings['test_secret_key']      = is_null( $test_secret_key ) ? $settings['test_secret_key'] : $test_secret_key;
+		$settings['test_webhook_secret']  = is_null( $test_webhook_secret ) ? $settings['test_webhook_secret'] : $test_webhook_secret;
+
+		if ( $new_account ) {
+			$settings['enabled'] = 'yes';
+			if ( trim( $settings['publishable_key'] ) && trim( $settings['secret_key'] ) ) {
+				$settings['testmode'] = 'no';
+			} elseif ( trim( $settings['test_publishable_key'] ) && trim( $settings['test_secret_key'] ) ) {
+				$settings['testmode'] = 'yes';
+			}
+		} elseif ( $is_deleting_account ) {
+			$settings['enabled'] = 'no';
+		}
+
+		update_option( self::STRIPE_GATEWAY_SETTINGS_OPTION_NAME, $settings );
+		$this->account->clear_cache();
+
+		// Gives an instant reply if the connection was succesful or not + rebuild the cache for the next request
+		$account = $this->account->get_cached_account_data();
+
+		return new WP_REST_Response( $account, 200 );
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-connection-tokens-controller.php b/includes/admin/class-wc-rest-stripe-connection-tokens-controller.php
new file mode 100644
index 0000000..b60e12c
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-connection-tokens-controller.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Connection_Tokens_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for connection tokens.
+ */
+class WC_REST_Stripe_Connection_Tokens_Controller extends WC_Stripe_REST_Base_Controller {
+
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/connection_tokens';
+
+	/**
+	 * Stripe payment gateway.
+	 *
+	 * @var WC_Gateway_Stripe
+	 */
+	private $gateway;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param WC_Gateway_Stripe $gateway Stripe payment gateway.
+	 */
+	public function __construct( WC_Gateway_Stripe $gateway ) {
+		$this->gateway = $gateway;
+	}
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => [ $this, 'create_token' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+	}
+
+	/**
+	 * Create a connection token via API.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function create_token( $request ) {
+		$response = WC_Stripe_API::request( [], 'terminal/connection_tokens' );
+
+		if ( ! isset( $response->secret ) ) {
+			return rest_ensure_response( new WP_Error( 'wc_stripe_no_token', __( 'Stripe API did not return a connection token.', 'woocommerce-gateway-stripe' ) ) );
+		}
+
+		$response->test_mode = $this->gateway->is_in_test_mode();
+		return rest_ensure_response( $response );
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-locations-controller.php b/includes/admin/class-wc-rest-stripe-locations-controller.php
new file mode 100644
index 0000000..e89846f
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-locations-controller.php
@@ -0,0 +1,267 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Locations_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for terminal locations.
+ */
+class WC_REST_Stripe_Locations_Controller extends WC_Stripe_REST_Base_Controller {
+
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/terminal/locations';
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => [ $this, 'create_location' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'display_name' => [
+						'type'     => 'string',
+						'required' => true,
+					],
+					'address'      => [
+						'type'     => 'object',
+						'required' => true,
+					],
+				],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_all_locations' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'ending_before'  => [
+						'type'     => 'string',
+						'required' => false,
+					],
+					'limit'          => [
+						'type'     => 'integer',
+						'required' => false,
+					],
+					'starting_after' => [
+						'type'     => 'string',
+						'required' => false,
+					],
+				],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/store',
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_store_location' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<location_id>\w+)',
+			[
+				'methods'             => WP_REST_Server::DELETABLE,
+				'callback'            => [ $this, 'delete_location' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<location_id>\w+)',
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_location' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<location_id>\w+)',
+			[
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => [ $this, 'update_location' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'display_name' => [
+						'type'     => 'string',
+						'required' => false,
+					],
+					'address'      => [
+						'type'     => 'object',
+						'required' => false,
+					],
+				],
+			]
+		);
+	}
+
+	/**
+	 * Create a terminal location via Stripe API.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function create_location( $request ) {
+		try {
+			$response = WC_Stripe_API::request(
+				[
+					'display_name' => $request['display_name'],
+					'address'      => $request['address'],
+				],
+				'terminal/locations'
+			);
+			return rest_ensure_response( $response );
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+	}
+
+	/**
+	 * Get all terminal locations via Stripe API.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function get_all_locations( $request ) {
+		try {
+			return rest_ensure_response( $this->fetch_locations() );
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+	}
+
+	/**
+	 * Delete a terminal location via Stripe API.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function delete_location( $request ) {
+		try {
+			$response = WC_Stripe_API::request( [], 'terminal/locations/' . urlencode( $request['location_id'] ), 'DELETE' );
+			return rest_ensure_response( $response );
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+	}
+
+	/**
+	 * Get a terminal location via Stripe API.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function get_location( $request ) {
+		try {
+			$response = WC_Stripe_API::request( [], 'terminal/locations/' . urlencode( $request['location_id'] ), 'GET' );
+			return rest_ensure_response( $response );
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+	}
+
+	/**
+	 * Get default terminal location.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function get_store_location( $request ) {
+		// Originally `get_bloginfo` was used for location name, later switched to `site_url` as the former may be blank.
+		$store_hostname = str_replace( [ 'https://', 'http://' ], '', get_site_url() );
+		$possible_names = [ get_bloginfo(), $store_hostname ];
+		$store_address = WC()->countries;
+		$address       = array_filter(
+			[
+				'city'        => $store_address->get_base_city(),
+				'country'     => $store_address->get_base_country(),
+				'line1'       => $store_address->get_base_address(),
+				'line2'       => $store_address->get_base_address_2(),
+				'postal_code' => $store_address->get_base_postcode(),
+				'state'       => $store_address->get_base_state(),
+			]
+		);
+
+		// Return an error if store doesn't have a location.
+		$is_address_populated = isset( $address['country'], $address['city'], $address['postal_code'], $address['line1'] );
+		if ( ! $is_address_populated ) {
+			return rest_ensure_response(
+				new WP_Error(
+					'store_address_is_incomplete',
+					admin_url(
+						add_query_arg(
+							[
+								'page' => 'wc-settings',
+								'tab'  => 'general',
+							],
+							'admin.php'
+						)
+					)
+				)
+			);
+		}
+
+		try {
+			foreach ( $this->fetch_locations() as $location ) {
+				if (
+					in_array( $location->display_name, $possible_names, true )
+					&& count( array_intersect( (array) $location->address, $address ) ) === count( $address )
+				) {
+					return rest_ensure_response( $location );
+				}
+			}
+
+			// Create new location if no location matches display name and address.
+			$response = WC_Stripe_API::request(
+				[
+					'display_name' => $store_hostname,
+					'address'      => $address,
+				],
+				'terminal/locations'
+			);
+			return rest_ensure_response( $response );
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+	}
+
+	/**
+	 * Update a terminal location via Stripe API.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function update_location( $request ) {
+		$body = [];
+		if ( isset( $request['display_name'] ) ) {
+			$body['display_name'] = $request['display_name'];
+		}
+		if ( isset( $request['address'] ) ) {
+			$body['address'] = $request['address'];
+		}
+		try {
+			$response = WC_Stripe_API::request( $body, 'terminal/locations/' . urlencode( $request['location_id'] ), 'POST' );
+			return rest_ensure_response( $response );
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+	}
+
+	/**
+	 * Fetch terminal locations from Stripe API.
+	 */
+	private function fetch_locations() {
+		$response = (array) WC_Stripe_API::request( [], 'terminal/locations', 'GET' );
+		return $response['data'];
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-orders-controller.php b/includes/admin/class-wc-rest-stripe-orders-controller.php
new file mode 100644
index 0000000..dddc1f0
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-orders-controller.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Orders_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for orders.
+ */
+class WC_REST_Stripe_Orders_Controller extends WC_Stripe_REST_Base_Controller {
+
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/orders';
+
+	/**
+	 * Stripe payment gateway.
+	 *
+	 * @var WC_Gateway_Stripe
+	 */
+	private $gateway;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param WC_Gateway_Stripe $gateway Stripe payment gateway.
+	 */
+	public function __construct( WC_Gateway_Stripe $gateway ) {
+		$this->gateway = $gateway;
+	}
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<order_id>\d+)/create_customer',
+			[
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => [ $this, 'create_customer' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<order_id>\w+)/capture_terminal_payment',
+			[
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => [ $this, 'capture_terminal_payment' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'payment_intent_id' => [
+						'required' => true,
+					],
+				],
+			]
+		);
+	}
+
+	/**
+	 * Create a Stripe customer for an order if needed, or return existing customer.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function create_customer( $request ) {
+		$order_id = $request['order_id'];
+
+		// Ensure order exists.
+		$order = wc_get_order( $order_id );
+		if ( false === $order || ! ( $order instanceof WC_Order ) ) {
+			return new WP_Error( 'wc_stripe', __( 'Order not found', 'woocommerce-gateway-stripe' ), [ 'status' => 404 ] );
+		}
+
+		// Validate order status before creating customer.
+		$disallowed_order_statuses = apply_filters( 'wc_stripe_create_customer_disallowed_order_statuses', [ 'completed', 'cancelled', 'refunded', 'failed' ] );
+		if ( $order->has_status( $disallowed_order_statuses ) ) {
+			return new WP_Error( 'wc_stripe_invalid_order_status', __( 'Invalid order status', 'woocommerce-gateway-stripe' ), [ 'status' => 400 ] );
+		}
+
+		// Get a customer object with the order's user, if available.
+		$order_user = $order->get_user();
+		if ( false === $order_user ) {
+			$order_user = new WP_User();
+		}
+		$customer = new WC_Stripe_Customer( $order_user->ID );
+
+		// Set the customer ID if known but not already set.
+		$customer_id = $order->get_meta( '_stripe_customer_id', true );
+		if ( ! $customer->get_id() && $customer_id ) {
+			$customer->set_id( $customer_id );
+		}
+
+		try {
+			// Update or create Stripe customer.
+			$customer_data = WC_Stripe_Customer::map_customer_data( $order );
+			if ( $customer->get_id() ) {
+				$customer_id = $customer->update_customer( $customer_data );
+			} else {
+				$customer_id = $customer->create_customer( $customer_data );
+			}
+		} catch ( WC_Stripe_Exception $e ) {
+			return new WP_Error( 'stripe_error', $e->getMessage() );
+		}
+
+		$order->update_meta_data( '_stripe_customer_id', $customer_id );
+		$order->save();
+
+		return rest_ensure_response( [ 'id' => $customer_id ] );
+	}
+
+	public function capture_terminal_payment( $request ) {
+		try {
+			$intent_id = $request['payment_intent_id'];
+			$order_id  = $request['order_id'];
+			$order     = wc_get_order( $order_id );
+
+			// Check that order exists before capturing payment.
+			if ( ! $order ) {
+				return new WP_Error( 'wc_stripe_missing_order', __( 'Order not found', 'woocommerce-gateway-stripe' ), [ 'status' => 404 ] );
+			}
+
+			// Do not process refunded orders.
+			if ( 0 < $order->get_total_refunded() ) {
+				return new WP_Error( 'wc_stripe_refunded_order_uncapturable', __( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-gateway-stripe' ), [ 'status' => 400 ] );
+			}
+
+			// Retrieve intent from Stripe.
+			$intent = WC_Stripe_API::retrieve( "payment_intents/$intent_id" );
+
+			// Check that intent exists.
+			if ( ! empty( $intent->error ) ) {
+				return new WP_Error( 'stripe_error', $intent->error->message );
+			}
+
+			// Ensure that intent can be captured.
+			if ( ! in_array( $intent->status, [ 'processing', 'requires_capture' ], true ) ) {
+				return new WP_Error( 'wc_stripe_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-gateway-stripe' ), [ 'status' => 409 ] );
+			}
+
+			// Update order with payment method and intent details.
+			$order->set_payment_method( WC_Gateway_Stripe::ID );
+			$order->set_payment_method_title( __( 'WooCommerce Stripe In-Person Payments', 'woocommerce-gateway-stripe' ) );
+			$this->gateway->save_intent_to_order( $order, $intent );
+
+			// Capture payment intent.
+			$charge = end( $intent->charges->data );
+			$this->gateway->process_response( $charge, $order );
+			$result = WC_Stripe_Order_Handler::get_instance()->capture_payment( $order );
+
+			// Check for failure to capture payment.
+			if ( empty( $result ) || empty( $result->status ) || 'succeeded' !== $result->status ) {
+				return new WP_Error(
+					'wc_stripe_capture_error',
+					sprintf(
+						// translators: %s: the error message.
+						__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-gateway-stripe' ),
+						$result->error->message ?? __( 'Unknown error', 'woocommerce-gateway-stripe' )
+					),
+					[ 'status' => 502 ]
+				);
+			}
+
+			// Successfully captured.
+			$order->update_status( 'completed' );
+			return rest_ensure_response(
+				[
+					'status' => $result->status,
+					'id'     => $result->id,
+				]
+			);
+		} catch ( WC_Stripe_Exception $e ) {
+			return rest_ensure_response( new WP_Error( 'stripe_error', $e->getMessage() ) );
+		}
+
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-payment-gateway-controller.php b/includes/admin/class-wc-rest-stripe-payment-gateway-controller.php
new file mode 100644
index 0000000..e439969
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-payment-gateway-controller.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Payment_Gateway_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Dynamic REST controller for payment gateway settings.
+ */
+class WC_REST_Stripe_Payment_Gateway_Controller extends WC_Stripe_REST_Base_Controller {
+
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/payment-gateway';
+
+	/**
+	 * Stripe payment gateway.
+	 *
+	 * @var WC_Gateway_Stripe
+	 */
+	private $gateway;
+
+	/**
+	 *  Gateway match array.
+	 *
+	 * @var array
+	 */
+	private $gateways = [
+		'stripe_sepa'       => WC_Gateway_Stripe_Sepa::class,
+		'stripe_giropay'    => WC_Gateway_Stripe_Giropay::class,
+		'stripe_ideal'      => WC_Gateway_Stripe_Ideal::class,
+		'stripe_bancontact' => WC_Gateway_Stripe_Bancontact::class,
+		'stripe_eps'        => WC_Gateway_Stripe_Eps::class,
+		'stripe_sofort'     => WC_Gateway_Stripe_Sofort::class,
+		'stripe_p24'        => WC_Gateway_Stripe_P24::class,
+		'stripe_alipay'     => WC_Gateway_Stripe_Alipay::class,
+		'stripe_multibanco' => WC_Gateway_Stripe_Multibanco::class,
+		'stripe_oxxo'       => WC_Gateway_Stripe_Oxxo::class,
+		'stripe_boleto'     => WC_Gateway_Stripe_Boleto::class,
+	];
+
+	/**
+	 * Returns an instance of some WC_Gateway_Stripe.
+	 *
+	 * @return void
+	 */
+	private function instantiate_gateway( $gateway_id ) {
+		$this->gateway = new $this->gateways[ $gateway_id ]();
+	}
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<payment_gateway_id>[a-z0-9_]+)',
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_payment_gateway_settings' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<payment_gateway_id>[a-z0-9_]+)',
+			[
+				'methods'             => WP_REST_Server::EDITABLE,
+				'callback'            => [ $this, 'update_payment_gateway_settings' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+	}
+
+	/**
+	 * Retrieve payment gateway settings.
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function get_payment_gateway_settings( $request = null ) {
+		try {
+			$id = $request->get_param( 'payment_gateway_id' );
+			$this->instantiate_gateway( $id );
+			$settings = [
+				'is_' . $id . '_enabled' => $this->gateway->is_enabled(),
+				$id . '_name'            => $this->gateway->get_option( 'title' ),
+				$id . '_description'     => $this->gateway->get_option( 'description' ),
+			];
+			if ( method_exists( $this->gateway, 'get_unique_settings' ) ) {
+				$settings = $this->gateway->get_unique_settings( $settings );
+			}
+			return new WP_REST_Response( $settings );
+		} catch ( Exception $exception ) {
+			return new WP_REST_Response( [ 'result' => 'bad_request' ], 400 );
+		}
+	}
+
+	/**
+	 * Update payment gateway settings.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	public function update_payment_gateway_settings( WP_REST_Request $request ) {
+		try {
+			$id = $request->get_param( 'payment_gateway_id' );
+			$this->instantiate_gateway( $id );
+			$this->update_is_gateway_enabled( $request );
+			$this->update_gateway_name( $request );
+			$this->update_gateway_description( $request );
+			if ( method_exists( $this->gateway, 'update_unique_settings' ) ) {
+				$this->gateway->update_unique_settings( $request );
+			}
+			return new WP_REST_Response( [], 200 );
+		} catch ( Exception $exception ) {
+			return new WP_REST_Response( [ 'result' => 'bad_request' ], 400 );
+		}
+	}
+
+	/**
+	 * Updates payment gateway enabled status.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_gateway_enabled( WP_REST_Request $request ) {
+		$field_name = 'is_' . $this->gateway->id . '_enabled';
+		$is_enabled = $request->get_param( $field_name );
+
+		if ( null === $is_enabled || ! is_bool( $is_enabled ) ) {
+			return;
+		}
+
+		if ( $is_enabled ) {
+			$this->gateway->enable();
+		} else {
+			$this->gateway->disable();
+		}
+	}
+
+	/**
+	 * Updates payment gateway title.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_gateway_name( WP_REST_Request $request ) {
+		$field_name = $this->gateway->id . '_name';
+		$name       = $request->get_param( $field_name );
+
+		if ( null === $name ) {
+			return;
+		}
+
+		$value = sanitize_text_field( $name );
+		$this->gateway->update_option( 'title', $value );
+	}
+
+	/**
+	 * Updates payment gateway description.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_gateway_description( WP_REST_Request $request ) {
+		$field_name  = $this->gateway->id . '_description';
+		$description = $request->get_param( $field_name );
+
+		if ( null === $description ) {
+			return;
+		}
+
+		$value = sanitize_text_field( $description );
+		$this->gateway->update_option( 'description', $value );
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-settings-controller.php b/includes/admin/class-wc-rest-stripe-settings-controller.php
new file mode 100644
index 0000000..82492d4
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-settings-controller.php
@@ -0,0 +1,593 @@
+<?php
+/**
+ * Class WC_REST_Stripe_Settings_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for settings.
+ */
+class WC_REST_Stripe_Settings_Controller extends WC_Stripe_REST_Base_Controller {
+
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/settings';
+
+	/**
+	 * Stripe payment gateway.
+	 *
+	 * @var WC_Gateway_Stripe
+	 */
+	private $gateway;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param WC_Gateway_Stripe $gateway Stripe payment gateway.
+	 */
+	public function __construct( WC_Gateway_Stripe $gateway ) {
+		$this->gateway = $gateway;
+	}
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		$form_fields = $this->gateway->get_form_fields();
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_settings' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::EDITABLE,
+				'callback'            => [ $this, 'update_settings' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'is_stripe_enabled'                => [
+						'description'       => __( 'If Stripe should be enabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'is_test_mode_enabled'             => [
+						'description'       => __( 'Stripe test mode setting.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'title'                            => [
+						'description'       => __( 'Stripe display title.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'title_upe'                        => [
+						'description'       => __( 'New checkout experience title.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'description'                      => [
+						'description'       => __( 'Stripe display description.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'enabled_payment_method_ids'       => [
+						'description'       => __( 'Payment method IDs that should be enabled. Other methods will be disabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'array',
+						'items'             => [
+							'type' => 'string',
+							'enum' => $this->gateway->get_upe_available_payment_methods(),
+						],
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'is_payment_request_enabled'       => [
+						'description'       => __( 'If Stripe express checkouts should be enabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'payment_request_button_type'      => [
+						'description'       => __( 'Express checkout button types.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'enum'              => array_keys( $form_fields['payment_request_button_type']['options'] ),
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'payment_request_button_theme'     => [
+						'description'       => __( 'Express checkout button themes.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'enum'              => array_keys( $form_fields['payment_request_button_theme']['options'] ),
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'payment_request_button_size'      => [
+						'description'       => __( 'Express checkout button sizes.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						// it can happen that `$form_fields['payment_request_button_size']` is empty (in tests) - fixing temporarily.
+						'enum'              => array_keys( isset( $form_fields['payment_request_button_size']['options'] ) ? $form_fields['payment_request_button_size']['options'] : [] ),
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'payment_request_button_locations' => [
+						'description'       => __( 'Express checkout locations that should be enabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'array',
+						'items'             => [
+							'type' => 'string',
+							'enum' => array_keys( $form_fields['payment_request_button_locations']['options'] ),
+						],
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'is_manual_capture_enabled'        => [
+						'description'       => __( 'If manual capture of charges should be enabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'is_saved_cards_enabled'           => [
+						'description'       => __( 'If "Saved cards" should be enabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'is_separate_card_form_enabled'    => [
+						'description'       => __( 'If credit card number field, expiry date field, and CVC field should be separate.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'statement_descriptor'             => [
+						'description'       => __( 'Bank account descriptor to be displayed in customers\' bank accounts.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => [ $this, 'validate_regular_statement_descriptor' ],
+					],
+					'is_short_statement_descriptor_enabled' => [
+						'description'       => __( 'When enabled, we\'ll include the order number for card and express checkout transactions.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+					'short_statement_descriptor'       => [
+						'description'       => __( 'We\'ll use the short version in combination with the customer order number.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'string',
+						'validate_callback' => [ $this, 'validate_short_statement_descriptor' ],
+					],
+					'is_debug_log_enabled'             => [
+						'description'       => __( 'When enabled, payment error logs will be saved to WooCommerce > Status > Logs.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+				],
+			]
+		);
+	}
+
+	/**
+	 * Validate the regular statement descriptor.
+	 *
+	 * @param mixed           $value The value being validated.
+	 * @param WP_REST_Request $request The request made.
+	 * @param string          $param The parameter name, used in error messages.
+	 * @return true|WP_Error
+	 */
+	public function validate_regular_statement_descriptor( $value, $request, $param ) {
+		return $this->validate_statement_descriptor( $value, $request, $param, 22 );
+	}
+
+	/**
+	 * Validate the short statement descriptor.
+	 *
+	 * @param mixed           $value The value being validated.
+	 * @param WP_REST_Request $request The request made.
+	 * @param string          $param The parameter name, used in error messages.
+	 * @return true|WP_Error
+	 */
+	public function validate_short_statement_descriptor( $value, $request, $param ) {
+		$is_short_account_statement_enabled = $request->get_param( 'is_short_statement_descriptor_enabled' );
+
+		// bypassing validation to avoid errors in the client, it won't be updated under this condition
+		if ( ! $is_short_account_statement_enabled ) {
+			return true;
+		}
+
+		return $this->validate_statement_descriptor( $value, $request, $param, 10 );
+	}
+
+	/**
+	 * Validate the statement descriptor argument.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param mixed           $value The value being validated.
+	 * @param WP_REST_Request $request The request made.
+	 * @param string          $param The parameter name, used in error messages.
+	 * @param int             $max_length Maximum statement length.
+	 * @return true|WP_Error
+	 */
+	public function validate_statement_descriptor( $value, $request, $param, $max_length ) {
+		$string_validation_result = rest_validate_request_arg( $value, $request, $param );
+		if ( true !== $string_validation_result ) {
+			return $string_validation_result;
+		}
+
+		// Relaxing validation because it's blocking the user from saving it when they're on another tab of the settings screen
+		// TODO: work that out with either a UX approach or handling the validations of each tab separately
+		if ( '' === $value ) {
+			return true;
+		}
+
+		try {
+			$this->gateway->validate_account_statement_descriptor_field( $param, $value, $max_length );
+		} catch ( Exception $exception ) {
+			return new WP_Error(
+				'rest_invalid_pattern',
+				$exception->getMessage()
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Retrieve settings.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function get_settings() {
+		return new WP_REST_Response(
+			[
+				/* Settings > General */
+				'is_stripe_enabled'                     => $this->gateway->is_enabled(),
+				'is_test_mode_enabled'                  => $this->gateway->is_in_test_mode(),
+				'title'                                 => $this->gateway->get_validated_option( 'title' ),
+				'title_upe'                             => $this->gateway->get_validated_option( 'title_upe' ),
+				'description'                           => $this->gateway->get_validated_option( 'description' ),
+
+				/* Settings > Payments accepted on checkout */
+				'enabled_payment_method_ids'            => $this->gateway->get_upe_enabled_payment_method_ids(),
+				'available_payment_method_ids'          => $this->gateway->get_upe_available_payment_methods(),
+
+				/* Settings > Express checkouts */
+				'is_payment_request_enabled'            => 'yes' === $this->gateway->get_option( 'payment_request' ),
+				'payment_request_button_type'           => $this->gateway->get_validated_option( 'payment_request_button_type' ),
+				'payment_request_button_theme'          => $this->gateway->get_validated_option( 'payment_request_button_theme' ),
+				'payment_request_button_size'           => $this->gateway->get_validated_option( 'payment_request_button_size' ),
+				'payment_request_button_locations'      => $this->gateway->get_validated_option( 'payment_request_button_locations' ),
+
+				/* Settings > Payments & transactions */
+				'is_manual_capture_enabled'             => ! $this->gateway->is_automatic_capture_enabled(),
+				'is_saved_cards_enabled'                => 'yes' === $this->gateway->get_option( 'saved_cards' ),
+				'is_separate_card_form_enabled'         => 'no' === $this->gateway->get_option( 'inline_cc_form' ),
+				'statement_descriptor'                  => $this->gateway->get_validated_option( 'statement_descriptor' ),
+				'is_short_statement_descriptor_enabled' => 'yes' === $this->gateway->get_option( 'is_short_statement_descriptor_enabled' ),
+				'short_statement_descriptor'            => $this->gateway->get_validated_option( 'short_statement_descriptor' ),
+
+				/* Settings > Advanced settings */
+				'is_debug_log_enabled'                  => 'yes' === $this->gateway->get_option( 'logging' ),
+				'is_upe_enabled'                        => WC_Stripe_Feature_Flags::is_upe_checkout_enabled(),
+			]
+		);
+	}
+
+	/**
+	 * Update settings.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function update_settings( WP_REST_Request $request ) {
+		/* Settings > General */
+		$this->update_is_stripe_enabled( $request );
+		$this->update_title( $request );
+		$this->update_title_upe( $request );
+		$this->update_description( $request );
+		$this->update_is_test_mode_enabled( $request );
+
+		/* Settings > Payments accepted on checkout */
+		$this->update_enabled_payment_methods( $request );
+
+		/* Settings > Express checkouts */
+		$this->update_is_payment_request_enabled( $request );
+		$this->update_payment_request_settings( $request );
+
+		/* Settings > Payments & transactions */
+		$this->update_is_manual_capture_enabled( $request );
+		$this->update_is_saved_cards_enabled( $request );
+		$this->update_is_separate_card_form_enabled( $request );
+		$this->update_account_statement_descriptor( $request );
+		$this->update_is_short_account_statement_enabled( $request );
+		$this->update_short_account_statement_descriptor( $request );
+
+		/* Settings > Advanced settings */
+		$this->update_is_debug_log_enabled( $request );
+		$this->update_is_upe_enabled( $request );
+
+		return new WP_REST_Response( [], 200 );
+	}
+
+	/**
+	 * Updates Stripe enabled status.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_stripe_enabled( WP_REST_Request $request ) {
+		$is_stripe_enabled = $request->get_param( 'is_stripe_enabled' );
+
+		if ( null === $is_stripe_enabled ) {
+			return;
+		}
+
+		if ( $is_stripe_enabled ) {
+			$this->gateway->enable();
+		} else {
+			$this->gateway->disable();
+		}
+	}
+
+	/**
+	 * Updates title.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_title( WP_REST_Request $request ) {
+		$title = $request->get_param( 'title' );
+
+		if ( null === $title ) {
+			return;
+		}
+
+		$this->gateway->update_validated_option( 'title', $title );
+	}
+
+	/**
+	 * Updates UPE title.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_title_upe( WP_REST_Request $request ) {
+		$title_upe = $request->get_param( 'title_upe' );
+
+		if ( null === $title_upe ) {
+			return;
+		}
+
+		$this->gateway->update_validated_option( 'title_upe', $title_upe );
+	}
+
+	/**
+	 * Updates description.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_description( WP_REST_Request $request ) {
+		$description = $request->get_param( 'description' );
+
+		if ( null === $description ) {
+			return;
+		}
+
+		$this->gateway->update_validated_option( 'description', $description );
+	}
+
+	/**
+	 * Updates Stripe test mode.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_test_mode_enabled( WP_REST_Request $request ) {
+		$is_test_mode_enabled = $request->get_param( 'is_test_mode_enabled' );
+
+		if ( null === $is_test_mode_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'testmode', $is_test_mode_enabled ? 'yes' : 'no' );
+	}
+
+	/**
+	 * Updates the "payment request" enable/disable settings.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_payment_request_enabled( WP_REST_Request $request ) {
+		$is_payment_request_enabled = $request->get_param( 'is_payment_request_enabled' );
+
+		if ( null === $is_payment_request_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'payment_request', $is_payment_request_enabled ? 'yes' : 'no' );
+	}
+
+	/**
+	 * Updates manual capture.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_manual_capture_enabled( WP_REST_Request $request ) {
+		$is_manual_capture_enabled = $request->get_param( 'is_manual_capture_enabled' );
+
+		if ( null === $is_manual_capture_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'capture', $is_manual_capture_enabled ? 'no' : 'yes' );
+	}
+
+	/**
+	 * Updates "saved cards" feature.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_saved_cards_enabled( WP_REST_Request $request ) {
+		$is_saved_cards_enabled = $request->get_param( 'is_saved_cards_enabled' );
+
+		if ( null === $is_saved_cards_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'saved_cards', $is_saved_cards_enabled ? 'yes' : 'no' );
+	}
+
+	/**
+	 * Updates "saved cards" feature.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_separate_card_form_enabled( WP_REST_Request $request ) {
+		$is_separate_card_form_enabled = $request->get_param( 'is_separate_card_form_enabled' );
+
+		if ( null === $is_separate_card_form_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'inline_cc_form', $is_separate_card_form_enabled ? 'no' : 'yes' );
+	}
+
+	/**
+	 * Updates account statement descriptor.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_account_statement_descriptor( WP_REST_Request $request ) {
+		$account_statement_descriptor = $request->get_param( 'statement_descriptor' );
+
+		if ( null === $account_statement_descriptor ) {
+			return;
+		}
+
+		$this->gateway->update_validated_option( 'statement_descriptor', $account_statement_descriptor );
+	}
+
+	/**
+	 * Updates whether short account statement should be used.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_short_account_statement_enabled( WP_REST_Request $request ) {
+		$is_short_account_statement_enabled = $request->get_param( 'is_short_statement_descriptor_enabled' );
+
+		if ( null === $is_short_account_statement_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'is_short_statement_descriptor_enabled', $is_short_account_statement_enabled ? 'yes' : 'no' );
+	}
+
+	/**
+	 * Updates short account statement descriptor.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_short_account_statement_descriptor( WP_REST_Request $request ) {
+		$is_short_account_statement_enabled = $request->get_param( 'is_short_statement_descriptor_enabled' );
+		$short_account_statement_descriptor = $request->get_param( 'short_statement_descriptor' );
+
+		// since we're bypassing the validation on the same condition, we shouldn't update it
+		if ( ! $is_short_account_statement_enabled ) {
+			return;
+		}
+
+		if ( null === $short_account_statement_descriptor ) {
+			return;
+		}
+
+		$this->gateway->update_validated_option( 'short_statement_descriptor', $short_account_statement_descriptor );
+	}
+
+	/**
+	 * Updates whether debug logging is enabled.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_debug_log_enabled( WP_REST_Request $request ) {
+		$is_debug_log_enabled = $request->get_param( 'is_debug_log_enabled' );
+
+		if ( null === $is_debug_log_enabled ) {
+			return;
+		}
+
+		$this->gateway->update_option( 'logging', $is_debug_log_enabled ? 'yes' : 'no' );
+
+	}
+
+	/**
+	 * Updates whether debug logging is enabled.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_is_upe_enabled( WP_REST_Request $request ) {
+		$is_upe_enabled = $request->get_param( 'is_upe_enabled' );
+
+		if ( null === $is_upe_enabled ) {
+			return;
+		}
+
+		$settings = get_option( 'woocommerce_stripe_settings', [] );
+		$settings[ WC_Stripe_Feature_Flags::UPE_CHECKOUT_FEATURE_ATTRIBUTE_NAME ] = $is_upe_enabled ? 'yes' : 'disabled';
+		update_option( 'woocommerce_stripe_settings', $settings );
+
+		// including the class again because otherwise it's not present.
+		if ( WC_Stripe_Inbox_Notes::are_inbox_notes_supported() ) {
+			require_once WC_STRIPE_PLUGIN_PATH . '/includes/notes/class-wc-stripe-upe-availability-note.php';
+			WC_Stripe_UPE_Availability_Note::possibly_delete_note();
+
+			require_once WC_STRIPE_PLUGIN_PATH . '/includes/notes/class-wc-stripe-upe-stripelink-note.php';
+			WC_Stripe_UPE_StripeLink_Note::possibly_delete_note();
+		}
+	}
+
+	/**
+	 * Updates appearance attributes of the payment request button.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_payment_request_settings( WP_REST_Request $request ) {
+		$attributes = [
+			'payment_request_button_type'      => 'payment_request_button_type',
+			'payment_request_button_size'      => 'payment_request_button_size',
+			'payment_request_button_theme'     => 'payment_request_button_theme',
+			'payment_request_button_locations' => 'payment_request_button_locations',
+		];
+
+		foreach ( $attributes as $request_key => $attribute ) {
+			if ( null === $request->get_param( $request_key ) ) {
+				continue;
+			}
+
+			$value = $request->get_param( $request_key );
+			$this->gateway->update_validated_option( $attribute, $value );
+		}
+	}
+
+	/**
+	 * Updates the list of enabled payment methods.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 */
+	private function update_enabled_payment_methods( WP_REST_Request $request ) {
+		// no need to update the payment methods, if the UPE checkout is not enabled
+		if ( ! WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
+			return;
+		}
+
+		$payment_method_ids_to_enable = $request->get_param( 'enabled_payment_method_ids' );
+
+		if ( null === $payment_method_ids_to_enable ) {
+			return;
+		}
+
+		$available_payment_methods = $this->gateway->get_upe_available_payment_methods();
+
+		$payment_method_ids_to_enable = array_values(
+			array_filter(
+				$payment_method_ids_to_enable,
+				function ( $payment_method ) use ( $available_payment_methods ) {
+					return in_array( $payment_method, $available_payment_methods, true );
+				}
+			)
+		);
+
+		$this->gateway->update_option( 'upe_checkout_experience_accepted_payments', $payment_method_ids_to_enable );
+	}
+}
diff --git a/includes/admin/class-wc-rest-stripe-tokens-controller.php b/includes/admin/class-wc-rest-stripe-tokens-controller.php
new file mode 100644
index 0000000..3dabb68
--- /dev/null
+++ b/includes/admin/class-wc-rest-stripe-tokens-controller.php
@@ -0,0 +1,59 @@
+<?php
+/***
+ * Class WC_REST_Stripe_Tokens_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for tokens.
+ */
+class WC_REST_Stripe_Tokens_Controller extends WC_Stripe_REST_Base_Controller {
+
+	/**
+	 * Endpoint path.
+	 */
+	protected $rest_base = 'wc_stripe/tokens';
+
+	/**
+	 * Register REST API routes for Stripe tokens.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			// For more info on Stripe tokens, see the following:
+			// https://stripe.com/docs/api/tokens/object
+			'/' . $this->rest_base . '/(?P<token_id>[a-z]{3}_[a-zA-Z0-9]{24})',
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_token' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+	}
+
+	/**
+	 * Retrieve a Stripe token, given a secret-key and token_id.
+	 *
+	 * @param WP_REST_Request $request Request object.
+	 *
+	 * @return WP_REST_Response Response object.
+	 */
+	public function get_token( $request ) {
+		$token_id   = $request->get_param( 'token_id' );
+		$secret_key = $request->get_header( 'X-WCStripe-Secret-Key' );
+
+		try {
+			WC_Stripe_API::set_secret_key( $secret_key );
+			$response = WC_Stripe_API::request( [], "tokens/$token_id", 'GET' );
+
+			if ( ! empty( $response->error ) ) {
+				return new WP_REST_Response( [ 'result' => 'bad_request' ], 400 );
+			}
+		} catch ( Exception $exception ) {
+			return new WP_REST_Response( [ 'result' => 'bad_request' ], 400 );
+		}
+
+		return new WP_REST_Response( $response, 200 );
+	}
+}
diff --git a/includes/admin/class-wc-stripe-admin-notices.php b/includes/admin/class-wc-stripe-admin-notices.php
new file mode 100644
index 0000000..6db9bf5
--- /dev/null
+++ b/includes/admin/class-wc-stripe-admin-notices.php
@@ -0,0 +1,445 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Class that represents admin notices.
+ *
+ * @since 4.1.0
+ */
+class WC_Stripe_Admin_Notices {
+	/**
+	 * Notices (array)
+	 *
+	 * @var array
+	 */
+	public $notices = [];
+
+	/**
+	 * Constructor
+	 *
+	 * @since 4.1.0
+	 */
+	public function __construct() {
+		add_action( 'admin_notices', [ $this, 'admin_notices' ] );
+		add_action( 'wp_loaded', [ $this, 'hide_notices' ] );
+		add_action( 'woocommerce_stripe_updated', [ $this, 'stripe_updated' ] );
+	}
+
+	/**
+	 * Allow this class and other classes to add slug keyed notices (to avoid duplication).
+	 *
+	 * @since 1.0.0
+	 * @version 4.0.0
+	 */
+	public function add_admin_notice( $slug, $class, $message, $dismissible = false ) {
+		$this->notices[ $slug ] = [
+			'class'       => $class,
+			'message'     => $message,
+			'dismissible' => $dismissible,
+		];
+	}
+
+	/**
+	 * Display any notices we've collected thus far.
+	 *
+	 * @since 1.0.0
+	 * @version 4.0.0
+	 */
+	public function admin_notices() {
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			return;
+		}
+
+		// Main Stripe payment method.
+		$this->stripe_check_environment();
+
+		// All other payment methods.
+		$this->payment_methods_check_environment();
+
+		foreach ( (array) $this->notices as $notice_key => $notice ) {
+			echo '<div class="' . esc_attr( $notice['class'] ) . '" style="position:relative;">';
+
+			if ( $notice['dismissible'] ) {
+				?>
+				<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-stripe-hide-notice', $notice_key ), 'wc_stripe_hide_notices_nonce', '_wc_stripe_notice_nonce' ) ); ?>" class="woocommerce-message-close notice-dismiss" style="position:relative;float:right;padding:9px 0px 9px 9px 9px;text-decoration:none;"></a>
+				<?php
+			}
+
+			echo '<p>';
+			echo wp_kses(
+				$notice['message'],
+				[
+					'a' => [
+						'href'   => [],
+						'target' => [],
+					],
+				]
+			);
+			echo '</p></div>';
+		}
+	}
+
+	/**
+	 * List of available payment methods.
+	 *
+	 * @since 4.1.0
+	 * @return array
+	 */
+	public function get_payment_methods() {
+		return [
+			'alipay'     => 'WC_Gateway_Stripe_Alipay',
+			'bancontact' => 'WC_Gateway_Stripe_Bancontact',
+			'eps'        => 'WC_Gateway_Stripe_EPS',
+			'giropay'    => 'WC_Gateway_Stripe_Giropay',
+			'ideal'      => 'WC_Gateway_Stripe_Ideal',
+			'multibanco' => 'WC_Gateway_Stripe_Multibanco',
+			'p24'        => 'WC_Gateway_Stripe_p24',
+			'sepa'       => 'WC_Gateway_Stripe_Sepa',
+			'sofort'     => 'WC_Gateway_Stripe_Sofort',
+			'boleto'     => 'WC_Gateway_Stripe_Boleto',
+			'oxxo'       => 'WC_Gateway_Stripe_Oxxo',
+		];
+	}
+
+	/**
+	 * The backup sanity check, in case the plugin is activated in a weird way,
+	 * or the environment changes after activation. Also handles upgrade routines.
+	 *
+	 * @since 1.0.0
+	 * @version 4.0.0
+	 */
+	public function stripe_check_environment() {
+		$show_style_notice   = get_option( 'wc_stripe_show_style_notice' );
+		$show_ssl_notice     = get_option( 'wc_stripe_show_ssl_notice' );
+		$show_keys_notice    = get_option( 'wc_stripe_show_keys_notice' );
+		$show_3ds_notice     = get_option( 'wc_stripe_show_3ds_notice' );
+		$show_phpver_notice  = get_option( 'wc_stripe_show_phpver_notice' );
+		$show_wcver_notice   = get_option( 'wc_stripe_show_wcver_notice' );
+		$show_curl_notice    = get_option( 'wc_stripe_show_curl_notice' );
+		$show_sca_notice     = get_option( 'wc_stripe_show_sca_notice' );
+		$changed_keys_notice = get_option( 'wc_stripe_show_changed_keys_notice' );
+		$options             = get_option( 'woocommerce_stripe_settings' );
+		$testmode            = ( isset( $options['testmode'] ) && 'yes' === $options['testmode'] ) ? true : false;
+		$test_pub_key        = isset( $options['test_publishable_key'] ) ? $options['test_publishable_key'] : '';
+		$test_secret_key     = isset( $options['test_secret_key'] ) ? $options['test_secret_key'] : '';
+		$live_pub_key        = isset( $options['publishable_key'] ) ? $options['publishable_key'] : '';
+		$live_secret_key     = isset( $options['secret_key'] ) ? $options['secret_key'] : '';
+		$three_d_secure      = isset( $options['three_d_secure'] ) && 'yes' === $options['three_d_secure'];
+
+		if ( isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) {
+			if ( empty( $show_3ds_notice ) && $three_d_secure ) {
+				$url = 'https://stripe.com/docs/payments/3d-secure#three-ds-radar';
+
+				$message = sprintf(
+				/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+					__( 'WooCommerce Stripe - We see that you had the "Require 3D secure when applicable" setting turned on. This setting is not available here anymore, because it is now replaced by Stripe Radar. You can learn more about it %1$shere%2$s ', 'woocommerce-gateway-stripe' ),
+					'<a href="' . $url . '" target="_blank">',
+					'</a>'
+				);
+
+				$this->add_admin_notice( '3ds', 'notice notice-warning', $message, true );
+			}
+
+			if ( empty( $show_style_notice ) ) {
+				$message = sprintf(
+				/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+					__( 'WooCommerce Stripe - We recently made changes to Stripe that may impact the appearance of your checkout. If your checkout has changed unexpectedly, please follow these %1$sinstructions%2$s to fix.', 'woocommerce-gateway-stripe' ),
+					'<a href="https://woocommerce.com/document/stripe/#new-checkout-experience" target="_blank">',
+					'</a>'
+				);
+
+				$this->add_admin_notice( 'style', 'notice notice-warning', $message, true );
+
+				return;
+			}
+
+			// @codeCoverageIgnoreStart
+			if ( empty( $show_phpver_notice ) ) {
+				if ( version_compare( phpversion(), WC_STRIPE_MIN_PHP_VER, '<' ) ) {
+					/* translators: 1) int version 2) int version */
+					$message = __( 'WooCommerce Stripe - The minimum PHP version required for this plugin is %1$s. You are running %2$s.', 'woocommerce-gateway-stripe' );
+
+					$this->add_admin_notice( 'phpver', 'error', sprintf( $message, WC_STRIPE_MIN_PHP_VER, phpversion() ), true );
+
+					return;
+				}
+			}
+
+			if ( empty( $show_wcver_notice ) ) {
+				if ( WC_Stripe_Helper::is_wc_lt( WC_STRIPE_FUTURE_MIN_WC_VER ) ) {
+					/* translators: 1) int version 2) int version */
+					$message = __( 'WooCommerce Stripe - This is the last version of the plugin compatible with WooCommerce %1$s. All future versions of the plugin will require WooCommerce %2$s or greater.', 'woocommerce-gateway-stripe' );
+					$this->add_admin_notice( 'wcver', 'notice notice-warning', sprintf( $message, WC_VERSION, WC_STRIPE_FUTURE_MIN_WC_VER ), true );
+				}
+			}
+
+			if ( empty( $show_curl_notice ) ) {
+				if ( ! function_exists( 'curl_init' ) ) {
+					$this->add_admin_notice( 'curl', 'notice notice-warning', __( 'WooCommerce Stripe - cURL is not installed.', 'woocommerce-gateway-stripe' ), true );
+				}
+			}
+
+			// @codeCoverageIgnoreEnd
+			if ( empty( $show_keys_notice ) ) {
+				$secret = WC_Stripe_API::get_secret_key();
+				// phpcs:ignore
+				$should_show_notice_on_page = ! ( isset( $_GET['page'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 0 === strpos( $_GET['section'], 'stripe' ) );
+
+				if ( empty( $secret ) && $should_show_notice_on_page ) {
+					$setting_link = $this->get_setting_link();
+
+					$notice_message = sprintf(
+					/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+						__( 'Stripe is almost ready. To get started, %1$sset your Stripe account keys%2$s.', 'woocommerce-gateway-stripe' ),
+						'<a href="' . $setting_link . '">',
+						'</a>'
+					);
+					$this->add_admin_notice( 'keys', 'notice notice-warning', $notice_message, true );
+				}
+
+				// Check if keys are entered properly per live/test mode.
+				if ( $testmode ) {
+					$is_test_pub_key    = ! empty( $test_pub_key ) && preg_match( '/^pk_test_/', $test_pub_key );
+					$is_test_secret_key = ! empty( $test_secret_key ) && preg_match( '/^[rs]k_test_/', $test_secret_key );
+					if ( ! $is_test_pub_key || ! $is_test_secret_key ) {
+						$setting_link = $this->get_setting_link();
+
+						$notice_message = sprintf(
+						/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+							__( 'Stripe is in test mode however your test keys may not be valid. Test keys start with pk_test and sk_test or rk_test. Please go to your settings and, %1$sset your Stripe account keys%2$s.', 'woocommerce-gateway-stripe' ),
+							'<a href="' . $setting_link . '">',
+							'</a>'
+						);
+
+						$this->add_admin_notice( 'keys', 'notice notice-error', $notice_message, true );
+					}
+				} else {
+					$is_live_pub_key    = ! empty( $live_pub_key ) && preg_match( '/^pk_live_/', $live_pub_key );
+					$is_live_secret_key = ! empty( $live_secret_key ) && preg_match( '/^[rs]k_live_/', $live_secret_key );
+					if ( ! $is_live_pub_key || ! $is_live_secret_key ) {
+						$setting_link = $this->get_setting_link();
+
+						$message = sprintf(
+						/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+							__( 'Stripe is in live mode however your live keys may not be valid. Live keys start with pk_live and sk_live or rk_live. Please go to your settings and, %1$sset your Stripe account keys%2$s.', 'woocommerce-gateway-stripe' ),
+							'<a href="' . $setting_link . '">',
+							'</a>'
+						);
+
+						$this->add_admin_notice( 'keys', 'notice notice-error', $message, true );
+					}
+				}
+
+				// Check if Stripe Account data was successfully fetched.
+				$account_data = WC_Stripe::get_instance()->account->get_cached_account_data();
+				if ( ! empty( $secret ) && empty( $account_data ) ) {
+					$setting_link = $this->get_setting_link();
+
+					$message = sprintf(
+					/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+						__( 'Your customers cannot use Stripe on checkout, because we couldn\'t connect to your account. Please go to your settings and, %1$sset your Stripe account keys%2$s.', 'woocommerce-gateway-stripe' ),
+						'<a href="' . $setting_link . '">',
+						'</a>'
+					);
+
+					$this->add_admin_notice( 'keys', 'notice notice-error', $message, true );
+				}
+			}
+
+			if ( empty( $show_ssl_notice ) ) {
+				// Show message if enabled and FORCE SSL is disabled and WordpressHTTPS plugin is not detected.
+				if ( ! wc_checkout_is_https() ) {
+					$message = sprintf(
+					/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+						__( 'Stripe is enabled, but a SSL certificate is not detected. Your checkout may not be secure! Please ensure your server has a valid %1$sSSL certificate%2$s.', 'woocommerce-gateway-stripe' ),
+						'<a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" target="_blank">',
+						'</a>'
+					);
+
+					$this->add_admin_notice( 'ssl', 'notice notice-warning', $message, true );
+				}
+			}
+
+			if ( empty( $show_sca_notice ) ) {
+				$message = sprintf(
+				/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+					__( 'Stripe is now ready for Strong Customer Authentication (SCA) and 3D Secure 2! %1$sRead about SCA%2$s.', 'woocommerce-gateway-stripe' ),
+					'<a href="https://woocommerce.com/posts/introducing-strong-customer-authentication-sca/" target="_blank">',
+					'</a>'
+				);
+
+				$this->add_admin_notice( 'sca', 'notice notice-success', $message, true );
+			}
+
+			if ( 'yes' === $changed_keys_notice ) {
+				$message = sprintf(
+				/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+					__( 'The public and/or secret keys for the Stripe gateway have been changed. This might cause errors for existing customers and saved payment methods. %1$sClick here to learn more%2$s.', 'woocommerce-gateway-stripe' ),
+					'<a href="https://woocommerce.com/document/stripe-fixing-customer-errors" target="_blank">',
+					'</a>'
+				);
+
+				$this->add_admin_notice( 'changed_keys', 'notice notice-warning', $message, true );
+			}
+		}
+	}
+
+	/**
+	 * Environment check for all other payment methods.
+	 *
+	 * @since 4.1.0
+	 */
+	public function payment_methods_check_environment() {
+		$payment_methods = $this->get_payment_methods();
+
+		foreach ( $payment_methods as $method => $class ) {
+			$show_notice = get_option( 'wc_stripe_show_' . $method . '_notice' );
+			$gateway     = new $class();
+
+			if ( 'yes' !== $gateway->enabled || 'no' === $show_notice ) {
+				continue;
+			}
+
+			if ( ! in_array( get_woocommerce_currency(), $gateway->get_supported_currency(), true ) ) {
+				/* translators: 1) Payment method, 2) List of supported currencies */
+				$this->add_admin_notice( $method, 'notice notice-error', sprintf( __( '%1$s is enabled - it requires store currency to be set to %2$s', 'woocommerce-gateway-stripe' ), $gateway->get_method_title(), implode( ', ', $gateway->get_supported_currency() ) ), true );
+			}
+		}
+
+		if ( ! WC_Stripe_Feature_Flags::is_upe_preview_enabled() || ! WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
+			return;
+		}
+
+		foreach ( WC_Stripe_UPE_Payment_Gateway::UPE_AVAILABLE_METHODS as $method_class ) {
+			if ( WC_Stripe_UPE_Payment_Method_CC::class === $method_class ) {
+				continue;
+			}
+			$method      = $method_class::STRIPE_ID;
+			$show_notice = get_option( 'wc_stripe_show_' . $method . '_upe_notice' );
+			$upe_method  = new $method_class();
+			if ( ! $upe_method->is_enabled() || 'no' === $show_notice ) {
+				continue;
+			}
+			if ( ! in_array( get_woocommerce_currency(), $upe_method->get_supported_currencies(), true ) ) {
+				/* translators: %1$s Payment method, %2$s List of supported currencies */
+				$this->add_admin_notice( $method . '_upe', 'notice notice-error', sprintf( __( '%1$s is enabled - it requires store currency to be set to %2$s', 'woocommerce-gateway-stripe' ), $upe_method->get_label(), implode( ', ', $upe_method->get_supported_currencies() ) ), true );
+			}
+		}
+	}
+
+	/**
+	 * Hides any admin notices.
+	 *
+	 * @since 4.0.0
+	 * @version 4.0.0
+	 */
+	public function hide_notices() {
+		if ( isset( $_GET['wc-stripe-hide-notice'] ) && isset( $_GET['_wc_stripe_notice_nonce'] ) ) {
+			if ( ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_stripe_notice_nonce'] ) ), 'wc_stripe_hide_notices_nonce' ) ) {
+				wp_die( __( 'Action failed. Please refresh the page and retry.', 'woocommerce-gateway-stripe' ) );
+			}
+
+			if ( ! current_user_can( 'manage_woocommerce' ) ) {
+				wp_die( __( 'Cheatin&#8217; huh?', 'woocommerce-gateway-stripe' ) );
+			}
+
+			$notice = wc_clean( wp_unslash( $_GET['wc-stripe-hide-notice'] ) );
+
+			switch ( $notice ) {
+				case 'style':
+					update_option( 'wc_stripe_show_style_notice', 'no' );
+					break;
+				case 'phpver':
+					update_option( 'wc_stripe_show_phpver_notice', 'no' );
+					break;
+				case 'wcver':
+					update_option( 'wc_stripe_show_wcver_notice', 'no' );
+					break;
+				case 'curl':
+					update_option( 'wc_stripe_show_curl_notice', 'no' );
+					break;
+				case 'ssl':
+					update_option( 'wc_stripe_show_ssl_notice', 'no' );
+					break;
+				case 'keys':
+					update_option( 'wc_stripe_show_keys_notice', 'no' );
+					break;
+				case '3ds':
+					update_option( 'wc_stripe_show_3ds_notice', 'no' );
+					break;
+				case 'alipay':
+					update_option( 'wc_stripe_show_alipay_notice', 'no' );
+					break;
+				case 'bancontact':
+					update_option( 'wc_stripe_show_bancontact_notice', 'no' );
+					break;
+				case 'eps':
+					update_option( 'wc_stripe_show_eps_notice', 'no' );
+					break;
+				case 'giropay':
+					update_option( 'wc_stripe_show_giropay_notice', 'no' );
+					break;
+				case 'ideal':
+					update_option( 'wc_stripe_show_ideal_notice', 'no' );
+					break;
+				case 'multibanco':
+					update_option( 'wc_stripe_show_multibanco_notice', 'no' );
+					break;
+				case 'p24':
+					update_option( 'wc_stripe_show_p24_notice', 'no' );
+					break;
+				case 'sepa':
+					update_option( 'wc_stripe_show_sepa_notice', 'no' );
+					break;
+				case 'sofort':
+					update_option( 'wc_stripe_show_sofort_notice', 'no' );
+					break;
+				case 'sca':
+					update_option( 'wc_stripe_show_sca_notice', 'no' );
+					break;
+				case 'changed_keys':
+					update_option( 'wc_stripe_show_changed_keys_notice', 'no' );
+					break;
+				default:
+					if ( false !== strpos( $notice, '_upe' ) ) {
+						update_option( 'wc_stripe_show_' . $notice . '_notice', 'no' );
+					}
+					break;
+			}
+		}
+	}
+
+	/**
+	 * Get setting link.
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return string Setting link
+	 */
+	public function get_setting_link() {
+		return esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=stripe&panel=settings' ) );
+	}
+
+	/**
+	 * Saves options in order to hide notices based on the gateway's version.
+	 *
+	 * @since 4.3.0
+	 */
+	public function stripe_updated() {
+		$previous_version = get_option( 'wc_stripe_version' );
+
+		// Only show the style notice if the plugin was installed and older than 4.1.4.
+		if ( empty( $previous_version ) || version_compare( $previous_version, '4.1.4', 'ge' ) ) {
+			update_option( 'wc_stripe_show_style_notice', 'no' );
+		}
+
+		// Only show the SCA notice on pre-4.3.0 installs.
+		if ( empty( $previous_version ) || version_compare( $previous_version, '4.3.0', 'ge' ) ) {
+			update_option( 'wc_stripe_show_sca_notice', 'no' );
+		}
+	}
+}
+
+new WC_Stripe_Admin_Notices();
diff --git a/includes/admin/class-wc-stripe-inbox-notes.php b/includes/admin/class-wc-stripe-inbox-notes.php
new file mode 100644
index 0000000..94e74b4
--- /dev/null
+++ b/includes/admin/class-wc-stripe-inbox-notes.php
@@ -0,0 +1,230 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Class that adds Inbox notifications.
+ *
+ * @since 4.5.4
+ */
+class WC_Stripe_Inbox_Notes {
+	const SUCCESS_NOTE_NAME = 'stripe-apple-pay-marketing-guide-holiday-2020';
+	const FAILURE_NOTE_NAME = 'stripe-apple-pay-domain-verification-needed';
+
+	const POST_SETUP_SUCCESS_ACTION    = 'wc_stripe_apple_pay_post_setup_success';
+	const CAMPAIGN_2020_CLEANUP_ACTION = 'wc_stripe_apple_pay_2020_cleanup';
+
+	public function __construct() {
+		add_action( self::POST_SETUP_SUCCESS_ACTION, [ self::class, 'create_marketing_note' ] );
+		add_action( self::CAMPAIGN_2020_CLEANUP_ACTION, [ self::class, 'cleanup_campaign_2020' ] );
+		add_action( 'admin_init', [ self::class, 'create_upe_notes' ] );
+
+		// Schedule a 2020 holiday campaign cleanup action if needed.
+		// First, check to see if we are still before the cutoff.
+		// We don't need to (re)schedule this after the cutoff.
+		if ( time() < self::get_campaign_2020_cutoff() ) {
+			// If we don't have the clean up action scheduled, add it.
+			if ( ! wp_next_scheduled( self::CAMPAIGN_2020_CLEANUP_ACTION ) ) {
+				wp_schedule_single_event( self::get_campaign_2020_cutoff(), self::CAMPAIGN_2020_CLEANUP_ACTION );
+			}
+		}
+	}
+
+	public static function are_inbox_notes_supported() {
+		if ( ! class_exists( 'WC_Data_Store' ) ) {
+			return false;
+		}
+
+		try {
+			WC_Data_Store::load( 'admin-note' );
+		} catch ( Exception $e ) {
+			return false;
+		}
+
+		return trait_exists( 'Automattic\WooCommerce\Admin\Notes\NoteTraits' ) && class_exists( 'Automattic\WooCommerce\Admin\Notes\Note' );
+	}
+
+	public static function create_upe_notes() {
+		if ( ! self::are_inbox_notes_supported() ) {
+			return;
+		}
+
+		require_once WC_STRIPE_PLUGIN_PATH . '/includes/notes/class-wc-stripe-upe-availability-note.php';
+		WC_Stripe_UPE_Availability_Note::init();
+
+		require_once WC_STRIPE_PLUGIN_PATH . '/includes/notes/class-wc-stripe-upe-stripelink-note.php';
+		WC_Stripe_UPE_StripeLink_Note::init( WC_Stripe::get_instance()->get_main_stripe_gateway() );
+	}
+
+	public static function get_campaign_2020_cutoff() {
+		return strtotime( '22 December 2020' );
+	}
+
+	public static function get_success_title() {
+		if ( time() < self::get_campaign_2020_cutoff() ) {
+			return __( 'Boost sales this holiday season with Apple Pay!', 'woocommerce-gateway-stripe' );
+		}
+
+		return __( 'Boost sales with Apple Pay!', 'woocommerce-gateway-stripe' );
+	}
+
+	/**
+	 * Manage notes to show after Apple Pay domain verification.
+	 */
+	public static function notify_on_apple_pay_domain_verification( $verification_complete ) {
+		$admin_notes_class = WC_Stripe_Woo_Compat_Utils::get_notes_class();
+		if ( ! class_exists( $admin_notes_class ) || ! class_exists( 'WC_Data_Store' ) ) {
+			return;
+		}
+
+		try {
+			$data_store       = WC_Data_Store::load( 'admin-note' );
+			$failure_note_ids = $data_store->get_notes_with_name( self::FAILURE_NOTE_NAME );
+			// Delete all previously created, soft deleted and unactioned failure notes (Legacy).
+			while ( ! empty( $failure_note_ids ) ) {
+				$note_id = array_pop( $failure_note_ids );
+				$note    = $admin_notes_class::get_note( $note_id );
+				$note->delete();
+			}
+		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine
+
+		if ( $verification_complete ) {
+			if ( self::should_show_marketing_note() && ! wp_next_scheduled( self::POST_SETUP_SUCCESS_ACTION ) ) {
+				wp_schedule_single_event( time() + DAY_IN_SECONDS, self::POST_SETUP_SUCCESS_ACTION );
+			}
+		} else {
+			// Create new note if verification failed.
+			self::create_failure_note();
+		}
+	}
+
+	/**
+	 * Whether conditions are right for the marketing note.
+	 */
+	public static function should_show_marketing_note() {
+		// Display to US merchants only.
+		$base_location = wc_get_base_location();
+		if ( ! $base_location || 'US' !== $base_location['country'] ) {
+			return false;
+		}
+
+		// Make sure Apple Pay is enabled and setup is successful.
+		$stripe_settings       = get_option( 'woocommerce_stripe_settings', [] );
+		$stripe_enabled        = isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled'];
+		$button_enabled        = isset( $stripe_settings['payment_request'] ) && 'yes' === $stripe_settings['payment_request'];
+		$verification_complete = isset( $stripe_settings['apple_pay_domain_set'] ) && 'yes' === $stripe_settings['apple_pay_domain_set'];
+		if ( ! $stripe_enabled || ! $button_enabled || ! $verification_complete ) {
+			return false;
+		}
+
+		// Make sure note doesn't already exist.
+		try {
+			$data_store       = WC_Data_Store::load( 'admin-note' );
+			$success_note_ids = $data_store->get_notes_with_name( self::SUCCESS_NOTE_NAME );
+			if ( ! empty( $success_note_ids ) ) {
+				return false;
+			}
+		} catch ( Exception $e ) {
+			return false; // If unable to check, assume it shouldn't show note.
+		}
+
+		return true;
+	}
+
+	/**
+	 * If conditions are right, show note promoting Apple Pay marketing guide.
+	 */
+	public static function create_marketing_note() {
+		// Make sure conditions for this note still hold.
+		if ( ! self::should_show_marketing_note() || ! self::are_inbox_notes_supported() ) {
+			return;
+		}
+
+		try {
+			$admin_note_class = WC_Stripe_Woo_Compat_Utils::get_note_class();
+			$note             = new $admin_note_class();
+			$note->set_title( self::get_success_title() );
+			$note->set_content( __( 'Now that you accept Apple Pay® with Stripe, you can increase conversion rates by letting your customers know that Apple Pay is available. Here’s a marketing guide to help you get started.', 'woocommerce-gateway-stripe' ) );
+			$note->set_type( $admin_note_class::E_WC_ADMIN_NOTE_MARKETING );
+			$note->set_name( self::SUCCESS_NOTE_NAME );
+			$note->set_source( 'woocommerce-gateway-stripe' );
+			$note->add_action(
+				'marketing-guide',
+				__( 'See marketing guide', 'woocommerce-gateway-stripe' ),
+				'https://developer.apple.com/apple-pay/marketing/'
+			);
+			$note->save();
+		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
+	}
+
+	/**
+	 * Show note indicating domain verification failure.
+	 */
+	public static function create_failure_note() {
+		try {
+			$admin_note_class = WC_Stripe_Woo_Compat_Utils::get_note_class();
+			$note             = new $admin_note_class();
+			$note->set_title( __( 'Apple Pay domain verification needed', 'woocommerce-gateway-stripe' ) );
+			$note->set_content( __( 'The WooCommerce Stripe Gateway extension attempted to perform domain verification on behalf of your store, but was unable to do so. This must be resolved before Apple Pay can be offered to your customers.', 'woocommerce-gateway-stripe' ) );
+			$note->set_type( $admin_note_class::E_WC_ADMIN_NOTE_INFORMATIONAL );
+			$note->set_name( self::FAILURE_NOTE_NAME );
+			$note->set_source( 'woocommerce-gateway-stripe' );
+			$note->add_action(
+				'learn-more',
+				__( 'Learn more', 'woocommerce-gateway-stripe' ),
+				'https://woocommerce.com/document/stripe/#apple-pay'
+			);
+			$note->save();
+		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
+	}
+
+	/**
+	 * Destroy unactioned inbox notes from the 2020 holiday campaign, replacing
+	 * them with a non-holiday note promoting Apple Pay. This will be run once
+	 * on/about 2020 Dec 22.
+	 */
+	public static function cleanup_campaign_2020() {
+		if ( ! self::are_inbox_notes_supported() ) {
+			return;
+		}
+
+		$admin_notes_class = WC_Stripe_Woo_Compat_Utils::get_notes_class();
+		if ( ! class_exists( $admin_notes_class ) || ! class_exists( 'WC_Data_Store' ) ) {
+			return;
+		}
+
+		$note_ids = [];
+
+		try {
+			$data_store = WC_Data_Store::load( 'admin-note' );
+			$note_ids   = $data_store->get_notes_with_name( self::SUCCESS_NOTE_NAME );
+			if ( empty( $note_ids ) ) {
+				return;
+			}
+		} catch ( Exception $e ) {
+			return;
+		}
+
+		$deleted_an_unactioned_note = false;
+
+		$admin_note_class = WC_Stripe_Woo_Compat_Utils::get_note_class();
+		foreach ( (array) $note_ids as $note_id ) {
+			try {
+				$note = new $admin_note_class( $note_id );
+				if ( $admin_note_class::E_WC_ADMIN_NOTE_UNACTIONED == $note->get_status() ) {
+					$note->delete();
+					$deleted_an_unactioned_note = true;
+				}
+				unset( $note );
+			} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
+		}
+
+		if ( $deleted_an_unactioned_note ) {
+			self::create_marketing_note();
+		}
+	}
+}
+
+new WC_Stripe_Inbox_Notes();
diff --git a/includes/admin/class-wc-stripe-old-settings-upe-toggle-controller.php b/includes/admin/class-wc-stripe-old-settings-upe-toggle-controller.php
new file mode 100644
index 0000000..651ff25
--- /dev/null
+++ b/includes/admin/class-wc-stripe-old-settings-upe-toggle-controller.php
@@ -0,0 +1,78 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Enqueues some JS to ensure that some needed UI elements for the old settings are available.
+ *
+ * @since 5.5.0
+ */
+class WC_Stripe_Old_Settings_UPE_Toggle_Controller {
+	protected $was_upe_checkout_enabled = null;
+
+	public function __construct() {
+		add_filter( 'pre_update_option_woocommerce_stripe_settings', [ $this, 'pre_options_save' ] );
+		add_action( 'update_option_woocommerce_stripe_settings', [ $this, 'maybe_enqueue_script' ] );
+	}
+
+	/**
+	 * Stores whether UPE was enabled before saving the options.
+	 *
+	 * @param mixed $value
+	 *
+	 * @return mixed
+	 */
+	public function pre_options_save( $value ) {
+		$this->was_upe_checkout_enabled = WC_Stripe_Feature_Flags::is_upe_checkout_enabled();
+
+		return $value;
+	}
+
+	/**
+	 * Determines what to do after the options have been saved.
+	 */
+	public function maybe_enqueue_script() {
+		$is_upe_checkout_enabled = WC_Stripe_Feature_Flags::is_upe_checkout_enabled();
+
+		if ( $this->was_upe_checkout_enabled !== $is_upe_checkout_enabled ) {
+			add_action( 'admin_enqueue_scripts', [ $this, 'upe_toggle_script' ] );
+		}
+	}
+
+	/**
+	 * Enqueues the script to determine what to do once UPE has been toggled.
+	 */
+	public function upe_toggle_script() {
+		// Webpack generates an assets file containing a dependencies array for our built JS file.
+		$script_asset_path = WC_STRIPE_PLUGIN_PATH . '/build/old_settings_upe_toggle.asset.php';
+		$script_asset      = file_exists( $script_asset_path )
+			? require $script_asset_path
+			: [
+				'dependencies' => [],
+				'version'      => WC_STRIPE_VERSION,
+			];
+
+		wp_register_script(
+			'woocommerce_stripe_old_settings_upe_toggle',
+			plugins_url( 'build/old_settings_upe_toggle.js', WC_STRIPE_MAIN_FILE ),
+			$script_asset['dependencies'],
+			$script_asset['version'],
+			true
+		);
+		wp_localize_script(
+			'woocommerce_stripe_old_settings_upe_toggle',
+			'wc_stripe_old_settings_param',
+			[
+				'was_upe_enabled' => $this->was_upe_checkout_enabled,
+				'is_upe_enabled'  => WC_Stripe_Feature_Flags::is_upe_checkout_enabled(),
+			]
+		);
+		wp_set_script_translations(
+			'woocommerce_stripe_old_settings_upe_toggle',
+			'woocommerce-gateway-stripe'
+		);
+		wp_enqueue_script( 'woocommerce_stripe_old_settings_upe_toggle' );
+	}
+}
diff --git a/includes/admin/class-wc-stripe-payment-gateways-controller.php b/includes/admin/class-wc-stripe-payment-gateways-controller.php
new file mode 100644
index 0000000..e80a827
--- /dev/null
+++ b/includes/admin/class-wc-stripe-payment-gateways-controller.php
@@ -0,0 +1,84 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
+/**
+ * Class that handles various admin tasks.
+ *
+ * @since 5.6.0
+ */
+class WC_Stripe_Payment_Gateways_Controller {
+	/**
+	 * Constructor
+	 *
+	 * @since 5.6.0
+	 */
+	public function __construct() {
+		// If UPE is enabled and there are enabled payment methods, we need to load the disable Stripe confirmation modal.
+		$stripe_settings              = get_option( 'woocommerce_stripe_settings', [] );
+		$enabled_upe_payment_methods  = isset( $stripe_settings['upe_checkout_experience_accepted_payments'] ) ? $stripe_settings['upe_checkout_experience_accepted_payments'] : [];
+		$upe_payment_requests_enabled = 'yes' === $stripe_settings['payment_request'];
+
+		if ( count( $enabled_upe_payment_methods ) > 0 || $upe_payment_requests_enabled ) {
+			add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ] );
+			add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'wc_stripe_gateway_container' ] );
+		}
+	}
+
+	public function register_payments_scripts() {
+		$payment_gateways_script_asset_path = WC_STRIPE_PLUGIN_PATH . '/build/payment_gateways.asset.php';
+		$payment_gateways_script_asset      = file_exists( $payment_gateways_script_asset_path )
+			? require_once $payment_gateways_script_asset_path
+			: [
+				'dependencies' => [],
+				'version'      => WC_STRIPE_VERSION,
+			];
+
+		wp_register_script(
+			'woocommerce_stripe_payment_gateways_page',
+			plugins_url( 'build/payment_gateways.js', WC_STRIPE_MAIN_FILE ),
+			$payment_gateways_script_asset['dependencies'],
+			$payment_gateways_script_asset['version'],
+			true
+		);
+		wp_set_script_translations(
+			'woocommerce_stripe_payment_gateways_page',
+			'woocommerce-gateway-stripe'
+		);
+		wp_register_style(
+			'woocommerce_stripe_payment_gateways_page',
+			plugins_url( 'build/payment_gateways.css', WC_STRIPE_MAIN_FILE ),
+			[ 'wc-components' ],
+			$payment_gateways_script_asset['version']
+		);
+	}
+
+	public function enqueue_payments_scripts() {
+		global $current_tab, $current_section;
+
+		$this->register_payments_scripts();
+
+		$is_payment_methods_page = (
+			is_admin() &&
+			$current_tab && ! $current_section
+			&& 'checkout' === $current_tab
+		);
+
+		if ( $is_payment_methods_page ) {
+			wp_enqueue_script( 'woocommerce_stripe_payment_gateways_page' );
+			wp_enqueue_style( 'woocommerce_stripe_payment_gateways_page' );
+		}
+	}
+
+	/**
+	 * Adds a container to the "payment gateways" page.
+	 * This is where the "Are you sure you want to disable Stripe?" confirmation dialog is rendered.
+	 */
+	public function wc_stripe_gateway_container() {
+		?><div id="wc-stripe-payment-gateways-container" />
+		<?php
+	}
+
+}
diff --git a/includes/admin/class-wc-stripe-payment-requests-controller.php b/includes/admin/class-wc-stripe-payment-requests-controller.php
new file mode 100644
index 0000000..16198ff
--- /dev/null
+++ b/includes/admin/class-wc-stripe-payment-requests-controller.php
@@ -0,0 +1,63 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Admin page for UPE Customize Express Checkouts.
+ *
+ * @since 5.4.1
+ */
+class WC_Stripe_Payment_Requests_Controller {
+	public function __construct() {
+		add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
+		add_action( 'wc_stripe_gateway_admin_options_wrapper', [ $this, 'admin_options' ] );
+	}
+
+	/**
+	 * Load admin scripts.
+	 */
+	public function admin_scripts() {
+		// Webpack generates an assets file containing a dependencies array for our built JS file.
+		$script_asset_path = WC_STRIPE_PLUGIN_PATH . '/build/payment_requests_settings.asset.php';
+		$asset_metadata    = file_exists( $script_asset_path )
+			? require $script_asset_path
+			: [
+				'dependencies' => [],
+				'version'      => WC_STRIPE_VERSION,
+			];
+		wp_register_script(
+			'wc-stripe-payment-request-settings',
+			plugins_url( 'build/payment_requests_settings.js', WC_STRIPE_MAIN_FILE ),
+			$asset_metadata['dependencies'],
+			$asset_metadata['version'],
+			true
+		);
+		wp_set_script_translations(
+			'wc-stripe-payment-request-settings',
+			'woocommerce-gateway-stripe'
+		);
+		wp_enqueue_script( 'wc-stripe-payment-request-settings' );
+
+		wp_register_style(
+			'wc-stripe-payment-request-settings',
+			plugins_url( 'build/payment_requests_settings.css', WC_STRIPE_MAIN_FILE ),
+			[ 'wc-components' ],
+			$asset_metadata['version']
+		);
+		wp_enqueue_style( 'wc-stripe-payment-request-settings' );
+	}
+
+	/**
+	 * Prints the admin options for the gateway.
+	 * Remove this action once we're fully migrated to UPE and move the wrapper in the `admin_options` method of the UPE gateway.
+	 */
+	public function admin_options() {
+		global $hide_save_button;
+		$hide_save_button = true;
+		echo '<h2>' . __( 'Customize express checkouts', 'woocommerce-gateway-stripe' );
+		wc_back_link( __( 'Return to Stripe', 'woocommerce-gateway-stripe' ), admin_url( 'admin.php?page=wc-settings&tab=checkout&section=stripe' ) );
+		echo '</h2>';
+		echo '<div class="wrap"><div id="wc-stripe-payment-request-settings-container"></div></div>';
+	}
+}
diff --git a/includes/admin/class-wc-stripe-privacy.php b/includes/admin/class-wc-stripe-privacy.php
new file mode 100644
index 0000000..369d995
--- /dev/null
+++ b/includes/admin/class-wc-stripe-privacy.php
@@ -0,0 +1,441 @@
+<?php
+if ( ! class_exists( 'WC_Abstract_Privacy' ) ) {
+	return;
+}
+
+class WC_Stripe_Privacy extends WC_Abstract_Privacy {
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		parent::__construct( __( 'Stripe', 'woocommerce-gateway-stripe' ) );
+
+		$this->add_exporter( 'woocommerce-gateway-stripe-order-data', __( 'WooCommerce Stripe Order Data', 'woocommerce-gateway-stripe' ), [ $this, 'order_data_exporter' ] );
+
+		if ( function_exists( 'wcs_get_subscriptions' ) ) {
+			$this->add_exporter( 'woocommerce-gateway-stripe-subscriptions-data', __( 'WooCommerce Stripe Subscriptions Data', 'woocommerce-gateway-stripe' ), [ $this, 'subscriptions_data_exporter' ] );
+		}
+
+		$this->add_exporter( 'woocommerce-gateway-stripe-customer-data', __( 'WooCommerce Stripe Customer Data', 'woocommerce-gateway-stripe' ), [ $this, 'customer_data_exporter' ] );
+
+		$this->add_eraser( 'woocommerce-gateway-stripe-customer-data', __( 'WooCommerce Stripe Customer Data', 'woocommerce-gateway-stripe' ), [ $this, 'customer_data_eraser' ] );
+		$this->add_eraser( 'woocommerce-gateway-stripe-order-data', __( 'WooCommerce Stripe Data', 'woocommerce-gateway-stripe' ), [ $this, 'order_data_eraser' ] );
+
+		add_filter( 'woocommerce_get_settings_account', [ $this, 'account_settings' ] );
+	}
+
+	/**
+	 * Add retention settings to account tab.
+	 *
+	 * @param array $settings
+	 * @return array $settings Updated
+	 */
+	public function account_settings( $settings ) {
+		$insert_setting = [
+			[
+				'title'       => __( 'Retain Stripe Data', 'woocommerce-gateway-stripe' ),
+				'desc_tip'    => __( 'Retains any Stripe data such as Stripe customer ID, source ID.', 'woocommerce-gateway-stripe' ),
+				'id'          => 'woocommerce_gateway_stripe_retention',
+				'type'        => 'relative_date_selector',
+				'placeholder' => __( 'N/A', 'woocommerce-gateway-stripe' ),
+				'default'     => '',
+				'autoload'    => false,
+			],
+		];
+
+		$index = null;
+
+		foreach ( $settings as $key => $value ) {
+			if ( 'sectionend' === $value['type'] && 'personal_data_retention' === $value['id'] ) {
+				$index = $key;
+				break;
+			}
+		}
+
+		if ( ! is_null( $index ) ) {
+			array_splice( $settings, $index, 0, $insert_setting );
+		}
+
+		return $settings;
+	}
+
+	/**
+	 * Returns a list of orders that are using one of Stripe's payment methods.
+	 *
+	 * @param string $email_address
+	 * @param int    $page
+	 *
+	 * @return array WP_Post
+	 */
+	protected function get_stripe_orders( $email_address, $page ) {
+		$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
+
+		$order_query = [
+			'payment_method' => [ 'stripe', 'stripe_alipay', 'stripe_bancontact', 'stripe_eps', 'stripe_giropay', 'stripe_ideal', 'stripe_multibanco', 'stripe_p24', 'stripe_sepa', 'stripe_sofort' ],
+			'limit'          => 10,
+			'page'           => $page,
+		];
+
+		if ( $user instanceof WP_User ) {
+			$order_query['customer_id'] = (int) $user->ID;
+		} else {
+			$order_query['billing_email'] = $email_address;
+		}
+
+		return wc_get_orders( $order_query );
+	}
+
+	/**
+	 * Gets the message of the privacy to display.
+	 */
+	public function get_privacy_message() {
+
+		$message = sprintf(
+		/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+			esc_html__( 'By using this extension, you may be storing personal data or sharing data with an external service. %1$sLearn more about how this works, including what you may want to include in your privacy policy%2$s.', 'woocommerce-gateway-stripe' ),
+			'<a href="https://woocommerce.com/document/privacy-payments/#section-3" target="_blank">',
+			'</a>'
+		);
+
+		return wpautop( $message );
+	}
+
+	/**
+	 * Handle exporting data for Orders.
+	 *
+	 * @param string $email_address E-mail address to export.
+	 * @param int    $page          Pagination of data.
+	 *
+	 * @return array
+	 */
+	public function order_data_exporter( $email_address, $page = 1 ) {
+		$done           = false;
+		$data_to_export = [];
+
+		$orders = $this->get_stripe_orders( $email_address, (int) $page );
+
+		$done = true;
+
+		if ( 0 < count( $orders ) ) {
+			foreach ( $orders as $order ) {
+				$data_to_export[] = [
+					'group_id'    => 'woocommerce_orders',
+					'group_label' => __( 'Orders', 'woocommerce-gateway-stripe' ),
+					'item_id'     => 'order-' . $order->get_id(),
+					'data'        => [
+						[
+							'name'  => __( 'Stripe payment id', 'woocommerce-gateway-stripe' ),
+							'value' => get_post_meta( $order->get_id(), '_stripe_source_id', true ),
+						],
+						[
+							'name'  => __( 'Stripe customer id', 'woocommerce-gateway-stripe' ),
+							'value' => get_post_meta( $order->get_id(), '_stripe_customer_id', true ),
+						],
+					],
+				];
+			}
+
+			$done = 10 > count( $orders );
+		}
+
+		return [
+			'data' => $data_to_export,
+			'done' => $done,
+		];
+	}
+
+	/**
+	 * Handle exporting data for Subscriptions.
+	 *
+	 * @param string $email_address E-mail address to export.
+	 * @param int    $page          Pagination of data.
+	 *
+	 * @return array
+	 */
+	public function subscriptions_data_exporter( $email_address, $page = 1 ) {
+		$done           = false;
+		$page           = (int) $page;
+		$data_to_export = [];
+
+		$meta_query = [
+			'relation' => 'AND',
+			[
+				'key'     => '_payment_method',
+				'value'   => [ 'stripe', 'stripe_alipay', 'stripe_bancontact', 'stripe_eps', 'stripe_giropay', 'stripe_ideal', 'stripe_multibanco', 'stripe_p24', 'stripe_sepa', 'stripe_sofort' ],
+				'compare' => 'IN',
+			],
+			[
+				'key'     => '_billing_email',
+				'value'   => $email_address,
+				'compare' => '=',
+			],
+		];
+
+		$subscription_query = [
+			'posts_per_page' => 10,
+			'page'           => $page,
+			'meta_query'     => $meta_query,
+		];
+
+		$subscriptions = wcs_get_subscriptions( $subscription_query );
+
+		$done = true;
+
+		if ( 0 < count( $subscriptions ) ) {
+			foreach ( $subscriptions as $subscription ) {
+				$data_to_export[] = [
+					'group_id'    => 'woocommerce_subscriptions',
+					'group_label' => __( 'Subscriptions', 'woocommerce-gateway-stripe' ),
+					'item_id'     => 'subscription-' . $subscription->get_id(),
+					'data'        => [
+						[
+							'name'  => __( 'Stripe payment id', 'woocommerce-gateway-stripe' ),
+							'value' => get_post_meta( $subscription->get_id(), '_stripe_source_id', true ),
+						],
+						[
+							'name'  => __( 'Stripe customer id', 'woocommerce-gateway-stripe' ),
+							'value' => get_post_meta( $subscription->get_id(), '_stripe_customer_id', true ),
+						],
+					],
+				];
+			}
+
+			$done = 10 > count( $subscriptions );
+		}
+
+		return [
+			'data' => $data_to_export,
+			'done' => $done,
+		];
+	}
+
+	/**
+	 * Finds and exports customer data by email address.
+	 *
+	 * @param string $email_address The user email address.
+	 * @param int    $page  Page.
+	 * @return array An array of personal data in name value pairs
+	 */
+	public function customer_data_exporter( $email_address, $page ) {
+		$user           = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
+		$data_to_export = [];
+
+		if ( $user instanceof WP_User ) {
+			$stripe_user = new WC_Stripe_Customer( $user->ID );
+
+			$data_to_export[] = [
+				'group_id'    => 'woocommerce_customer',
+				'group_label' => __( 'Customer Data', 'woocommerce-gateway-stripe' ),
+				'item_id'     => 'user',
+				'data'        => [
+					[
+						'name'  => __( 'Stripe payment id', 'woocommerce-gateway-stripe' ),
+						'value' => get_user_option( '_stripe_source_id', $user->ID ),
+					],
+					[
+						'name'  => __( 'Stripe customer id', 'woocommerce-gateway-stripe' ),
+						'value' => $stripe_user->get_id(),
+					],
+				],
+			];
+		}
+
+		return [
+			'data' => $data_to_export,
+			'done' => true,
+		];
+	}
+
+	/**
+	 * Finds and erases customer data by email address.
+	 *
+	 * @param string $email_address The user email address.
+	 * @param int    $page  Page.
+	 * @return array An array of personal data in name value pairs
+	 */
+	public function customer_data_eraser( $email_address, $page ) {
+		$page               = (int) $page;
+		$user               = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
+		$stripe_customer_id = '';
+		$stripe_source_id   = '';
+
+		if ( $user instanceof WP_User ) {
+			$stripe_customer_id = get_user_option( '_stripe_customer_id', $user->ID );
+			$stripe_source_id   = get_user_option( '_stripe_source_id', $user->ID );
+		}
+
+		$items_removed = false;
+		$messages      = [];
+
+		if ( ! empty( $stripe_customer_id ) || ! empty( $stripe_source_id ) ) {
+			$items_removed = true;
+			delete_user_option( $user->ID, '_stripe_customer_id' );
+			delete_user_option( $user->ID, '_stripe_source_id' );
+			$messages[] = __( 'Stripe User Data Erased.', 'woocommerce-gateway-stripe' );
+		}
+
+		return [
+			'items_removed'  => $items_removed,
+			'items_retained' => false,
+			'messages'       => $messages,
+			'done'           => true,
+		];
+	}
+
+	/**
+	 * Finds and erases order data by email address.
+	 *
+	 * @param string $email_address The user email address.
+	 * @param int    $page  Page.
+	 * @return array An array of personal data in name value pairs
+	 */
+	public function order_data_eraser( $email_address, $page ) {
+		$orders = $this->get_stripe_orders( $email_address, (int) $page );
+
+		$items_removed  = false;
+		$items_retained = false;
+		$messages       = [];
+
+		foreach ( (array) $orders as $order ) {
+			$order = wc_get_order( $order->get_id() );
+
+			list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order );
+			$items_removed                    |= $removed;
+			$items_retained                   |= $retained;
+			$messages                          = array_merge( $messages, $msgs );
+
+			list( $removed, $retained, $msgs ) = $this->maybe_handle_subscription( $order );
+			$items_removed                    |= $removed;
+			$items_retained                   |= $retained;
+			$messages                          = array_merge( $messages, $msgs );
+		}
+
+		// Tell core if we have more orders to work on still
+		$done = count( $orders ) < 10;
+
+		return [
+			'items_removed'  => $items_removed,
+			'items_retained' => $items_retained,
+			'messages'       => $messages,
+			'done'           => $done,
+		];
+	}
+
+	/**
+	 * Handle eraser of data tied to Subscriptions
+	 *
+	 * @param WC_Order $order
+	 * @return array
+	 */
+	protected function maybe_handle_subscription( $order ) {
+		if ( ! class_exists( 'WC_Subscriptions' ) ) {
+			return [ false, false, [] ];
+		}
+
+		if ( ! wcs_order_contains_subscription( $order ) ) {
+			return [ false, false, [] ];
+		}
+
+		$subscription    = current( wcs_get_subscriptions_for_order( $order->get_id() ) );
+		$subscription_id = $subscription->get_id();
+
+		$stripe_source_id = get_post_meta( $subscription_id, '_stripe_source_id', true );
+
+		if ( empty( $stripe_source_id ) ) {
+			return [ false, false, [] ];
+		}
+
+		if ( ! $this->is_retention_expired( $order->get_date_created()->getTimestamp() ) ) {
+			/* translators: %d Order ID */
+			return [ false, true, [ sprintf( __( 'Order ID %d is less than set retention days. Personal data retained. (Stripe)', 'woocommerce-gateway-stripe' ), $order->get_id() ) ] ];
+		}
+
+		if ( $subscription->has_status( apply_filters( 'wc_stripe_privacy_eraser_subs_statuses', [ 'on-hold', 'active' ] ) ) ) {
+			/* translators: %d Order ID */
+			return [ false, true, [ sprintf( __( 'Order ID %d contains an active Subscription. Personal data retained. (Stripe)', 'woocommerce-gateway-stripe' ), $order->get_id() ) ] ];
+		}
+
+		$renewal_orders = WC_Subscriptions_Renewal_Order::get_renewal_orders( $order->get_id() );
+
+		foreach ( $renewal_orders as $renewal_order_id ) {
+			delete_post_meta( $renewal_order_id, '_stripe_source_id' );
+			delete_post_meta( $renewal_order_id, '_stripe_refund_id' );
+			delete_post_meta( $renewal_order_id, '_stripe_customer_id' );
+		}
+
+		delete_post_meta( $subscription_id, '_stripe_source_id' );
+		delete_post_meta( $subscription_id, '_stripe_refund_id' );
+		delete_post_meta( $subscription_id, '_stripe_customer_id' );
+
+		return [ true, false, [ __( 'Stripe Subscription Data Erased.', 'woocommerce-gateway-stripe' ) ] ];
+	}
+
+	/**
+	 * Handle eraser of data tied to Orders
+	 *
+	 * @param WC_Order $order
+	 * @return array
+	 */
+	protected function maybe_handle_order( $order ) {
+		$order_id           = $order->get_id();
+		$stripe_source_id   = get_post_meta( $order_id, '_stripe_source_id', true );
+		$stripe_refund_id   = get_post_meta( $order_id, '_stripe_refund_id', true );
+		$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
+
+		if ( ! $this->is_retention_expired( $order->get_date_created()->getTimestamp() ) ) {
+			/* translators: %d Order ID */
+			return [ false, true, [ sprintf( __( 'Order ID %d is less than set retention days. Personal data retained. (Stripe)', 'woocommerce-gateway-stripe' ), $order->get_id() ) ] ];
+		}
+
+		if ( empty( $stripe_source_id ) && empty( $stripe_refund_id ) && empty( $stripe_customer_id ) ) {
+			return [ false, false, [] ];
+		}
+
+		delete_post_meta( $order_id, '_stripe_source_id' );
+		delete_post_meta( $order_id, '_stripe_refund_id' );
+		delete_post_meta( $order_id, '_stripe_customer_id' );
+
+		return [ true, false, [ __( 'Stripe personal data erased.', 'woocommerce-gateway-stripe' ) ] ];
+	}
+
+	/**
+	 * Checks if create date is passed retention duration.
+	 */
+	public function is_retention_expired( $created_date ) {
+		$retention  = wc_parse_relative_date_option( get_option( 'woocommerce_gateway_stripe_retention' ) );
+		$is_expired = false;
+		$time_span  = time() - strtotime( $created_date );
+		if ( empty( $retention['number'] ) || empty( $created_date ) ) {
+			return false;
+		}
+		switch ( $retention['unit'] ) {
+			case 'days':
+				$retention = $retention['number'] * DAY_IN_SECONDS;
+				if ( $time_span > $retention ) {
+					$is_expired = true;
+				}
+				break;
+			case 'weeks':
+				$retention = $retention['number'] * WEEK_IN_SECONDS;
+				if ( $time_span > $retention ) {
+					$is_expired = true;
+				}
+				break;
+			case 'months':
+				$retention = $retention['number'] * MONTH_IN_SECONDS;
+				if ( $time_span > $retention ) {
+					$is_expired = true;
+				}
+				break;
+			case 'years':
+				$retention = $retention['number'] * YEAR_IN_SECONDS;
+				if ( $time_span > $retention ) {
+					$is_expired = true;
+				}
+				break;
+		}
+		return $is_expired;
+	}
+}
+
+new WC_Stripe_Privacy();
diff --git a/includes/admin/class-wc-stripe-rest-base-controller.php b/includes/admin/class-wc-stripe-rest-base-controller.php
new file mode 100644
index 0000000..5cf0d33
--- /dev/null
+++ b/includes/admin/class-wc-stripe-rest-base-controller.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Class WC_Stripe_REST_Base_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for transactions.
+ */
+class WC_Stripe_REST_Base_Controller extends WP_REST_Controller {
+
+	/**
+	 * Endpoint namespace.
+	 *
+	 * @var string
+	 */
+	protected $namespace = 'wc/v3';
+
+	/**
+	 * Verify access.
+	 *
+	 * Override this method if custom permissions required.
+	 */
+	public function check_permission() {
+		return current_user_can( 'manage_woocommerce' );
+	}
+}
diff --git a/includes/admin/class-wc-stripe-rest-upe-flag-toggle-controller.php b/includes/admin/class-wc-stripe-rest-upe-flag-toggle-controller.php
new file mode 100644
index 0000000..68c652a
--- /dev/null
+++ b/includes/admin/class-wc-stripe-rest-upe-flag-toggle-controller.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Class WC_Stripe_REST_UPE_Flag_Toggle_Controller
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * REST controller for UPE feature flag.
+ */
+class WC_Stripe_REST_UPE_Flag_Toggle_Controller extends WC_Stripe_REST_Base_Controller {
+	/**
+	 * Endpoint path.
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'wc_stripe/upe_flag_toggle';
+
+	/**
+	 * Configure REST API routes.
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => [ $this, 'get_flag' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+			]
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			[
+				'methods'             => WP_REST_Server::EDITABLE,
+				'callback'            => [ $this, 'set_flag' ],
+				'permission_callback' => [ $this, 'check_permission' ],
+				'args'                => [
+					'is_upe_enabled' => [
+						'description'       => __( 'Determines if the UPE feature flag is enabled.', 'woocommerce-gateway-stripe' ),
+						'type'              => 'boolean',
+						'validate_callback' => 'rest_validate_request_arg',
+					],
+				],
+			]
+		);
+	}
+
+	/**
+	 * Retrieve flag status.
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function get_flag() {
+		return new WP_REST_Response(
+			[
+				'is_upe_enabled' => WC_Stripe_Feature_Flags::is_upe_checkout_enabled(),
+			]
+		);
+	}
+
+	/**
+	 * Update the data.
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 */
+	public function set_flag( WP_REST_Request $request ) {
+		$is_upe_enabled = $request->get_param( 'is_upe_enabled' );
+
+		if ( null === $is_upe_enabled ) {
+			return new WP_REST_Response( [ 'result' => 'bad_request' ], 400 );
+		}
+
+		$settings = get_option( 'woocommerce_stripe_settings', [] );
+		$settings[ WC_Stripe_Feature_Flags::UPE_CHECKOUT_FEATURE_ATTRIBUTE_NAME ] = $is_upe_enabled ? 'yes' : 'disabled';
+
+		update_option( 'woocommerce_stripe_settings', $settings );
+
+		// including the class again because otherwise it's not present.
+		if ( WC_Stripe_Inbox_Notes::are_inbox_notes_supported() ) {
+			require_once WC_STRIPE_PLUGIN_PATH . '/includes/notes/class-wc-stripe-upe-availability-note.php';
+			WC_Stripe_UPE_Availability_Note::possibly_delete_note();
+
+			require_once WC_STRIPE_PLUGIN_PATH . '/includes/notes/class-wc-stripe-upe-stripelink-note.php';
+			WC_Stripe_UPE_StripeLink_Note::possibly_delete_note();
+		}
+
+		return new WP_REST_Response( [ 'result' => 'success' ], 200 );
+	}
+}
diff --git a/includes/admin/class-wc-stripe-settings-controller.php b/includes/admin/class-wc-stripe-settings-controller.php
new file mode 100644
index 0000000..93a136b
--- /dev/null
+++ b/includes/admin/class-wc-stripe-settings-controller.php
@@ -0,0 +1,131 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Controls whether we're on the settings page and enqueues the JS code.
+ *
+ * @since 5.4.1
+ */
+class WC_Stripe_Settings_Controller {
+	/**
+	 * The Stripe account instance.
+	 *
+	 * @var WC_Stripe_Account
+	 */
+	private $account;
+
+	/**
+	 * Constructor
+	 *
+	 * @param WC_Stripe_Account $account Stripe account
+	 */
+	public function __construct( WC_Stripe_Account $account ) {
+		$this->account = $account;
+		add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
+		add_action( 'wc_stripe_gateway_admin_options_wrapper', [ $this, 'admin_options' ] );
+	}
+
+	/**
+	 * Prints the admin options for the gateway.
+	 * Remove this action once we're fully migrated to UPE and move the wrapper in the `admin_options` method of the UPE gateway.
+	 *
+	 * @param WC_Stripe_Payment_Gateway $gateway the Stripe gateway.
+	 */
+	public function admin_options( WC_Stripe_Payment_Gateway $gateway ) {
+		global $hide_save_button;
+		$hide_save_button = true;
+
+		echo '<h2>' . esc_html( $gateway->get_method_title() );
+		wc_back_link( __( 'Return to payments', 'woocommerce-gateway-stripe' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) );
+		echo '</h2>';
+
+		$settings = get_option( WC_Stripe_Connect::SETTINGS_OPTION, [] );
+
+		$account_data_exists = ( ! empty( $settings['publishable_key'] ) && ! empty( $settings['secret_key'] ) ) || ( ! empty( $settings['test_publishable_key'] ) && ! empty( $settings['test_secret_key'] ) );
+		echo $account_data_exists ? '<div id="wc-stripe-account-settings-container"></div>' : '<div id="wc-stripe-new-account-container"></div>';
+	}
+
+	/**
+	 * Load admin scripts.
+	 */
+	public function admin_scripts( $hook_suffix ) {
+		if ( 'woocommerce_page_wc-settings' !== $hook_suffix ) {
+			return;
+		}
+
+		// TODO: refactor this to a regex approach, we will need to touch `should_enqueue_in_current_tab_section` to support it
+		if ( ! ( WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_sepa' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_giropay' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_ideal' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_bancontact' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_eps' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_sofort' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_p24' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_alipay' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_multibanco' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_oxxo' )
+			|| WC_Stripe_Helper::should_enqueue_in_current_tab_section( 'checkout', 'stripe_boleto' ) ) ) {
+			return;
+		}
+
+		$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+
+		// Webpack generates an assets file containing a dependencies array for our built JS file.
+		$script_asset_path = WC_STRIPE_PLUGIN_PATH . '/build/upe_settings.asset.php';
+		$script_asset      = file_exists( $script_asset_path )
+			? require $script_asset_path
+			: [
+				'dependencies' => [],
+				'version'      => WC_STRIPE_VERSION,
+			];
+
+		wp_register_script(
+			'woocommerce_stripe_admin',
+			plugins_url( 'build/upe_settings.js', WC_STRIPE_MAIN_FILE ),
+			$script_asset['dependencies'],
+			$script_asset['version'],
+			true
+		);
+		wp_register_style(
+			'woocommerce_stripe_admin',
+			plugins_url( 'build/upe_settings.css', WC_STRIPE_MAIN_FILE ),
+			[ 'wc-components' ],
+			$script_asset['version']
+		);
+
+		$oauth_url = woocommerce_gateway_stripe()->connect->get_oauth_url();
+		if ( is_wp_error( $oauth_url ) ) {
+			$oauth_url = '';
+		}
+
+		$message = sprintf(
+		/* translators: 1) Html strong opening tag 2) Html strong closing tag */
+			esc_html__( '%1$sWarning:%2$s your site\'s time does not match the time on your browser and may be incorrect. Some payment methods depend on webhook verification and verifying webhooks with a signing secret depends on your site\'s time being correct, so please check your site\'s time before setting a webhook secret. You may need to contact your site\'s hosting provider to correct the site\'s time.', 'woocommerce-gateway-stripe' ),
+			'<strong>',
+			'</strong>'
+		);
+
+		$params = [
+			'time'                    => time(),
+			'i18n_out_of_sync'        => $message,
+			'is_upe_checkout_enabled' => WC_Stripe_Feature_Flags::is_upe_checkout_enabled(),
+			'stripe_oauth_url'        => $oauth_url,
+		];
+		wp_localize_script(
+			'woocommerce_stripe_admin',
+			'wc_stripe_settings_params',
+			$params
+		);
+		wp_set_script_translations(
+			'woocommerce_stripe_admin',
+			'woocommerce-gateway-stripe'
+		);
+
+		wp_enqueue_script( 'woocommerce_stripe_admin' );
+		wp_enqueue_style( 'woocommerce_stripe_admin' );
+	}
+}
diff --git a/includes/admin/class-wc-stripe-upe-compatibility-controller.php b/includes/admin/class-wc-stripe-upe-compatibility-controller.php
new file mode 100644
index 0000000..e0792b8
--- /dev/null
+++ b/includes/admin/class-wc-stripe-upe-compatibility-controller.php
@@ -0,0 +1,124 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Controls the UPE compatibility before we release UPE - adds some notices for the merchant if necessary.
+ *
+ * @since 5.5.0
+ */
+class WC_Stripe_UPE_Compatibility_Controller {
+	public function __construct() {
+		add_action( 'admin_notices', [ $this, 'add_compatibility_notice' ] );
+	}
+
+	/**
+	 * I created this as a separate method, so it can be mocked in unit tests.
+	 *
+	 * @retun string
+	 */
+	public function get_wc_version() {
+		return WC_VERSION;
+	}
+
+	/**
+	 * Adds a compatibility notice in the `wp-admin` area in case the version of WooCommerce or WordPress are (or will be) not supported.
+	 */
+	public function add_compatibility_notice() {
+		/*
+		 * The following might be hard to read, but here's what I'm trying to do:
+		 * - If WP and WC are both supported -> nothing to do
+		 * - If WC is not supported -> construct message saying "Stripe requires WooCommerce 5.4 or greater to be installed and active. Your version of WooCommerce [X.X] is no longer be supported"
+		 * - If WP is not supported -> construct message saying "Stripe requires WordPress 5.6 or greater. Your version of WordPress [X.X] is no longer be supported"
+		 * - If WC & WP are both not supported -> construct message saying "Stripe requires WordPress 5.6 or greater and WooCommerce 5.4 or greater to be installed and active. Your versions of WordPress [X.X] and WooCommerce [X.X] are no longer be supported"
+		 */
+		$unsatisfied_requirements = array_filter(
+			[
+				[
+					'name'         => 'WordPress',
+					'version'      => get_bloginfo( 'version' ),
+					'is_supported' => WC_Stripe_UPE_Compatibility::is_wp_supported(),
+					/* translators: %s. WordPress version installed. */
+					'message'      => sprintf( __( 'WordPress %s or greater', 'woocommerce-gateway-stripe' ), WC_Stripe_UPE_Compatibility::MIN_WP_VERSION ),
+				],
+				[
+					'name'         => 'WooCommerce',
+					'version'      => $this->get_wc_version(),
+					'is_supported' => version_compare( $this->get_wc_version(), WC_Stripe_UPE_Compatibility::MIN_WC_VERSION, '>=' ),
+					'message'      => sprintf(
+					/* translators: %s. WooCommerce version installed. */
+						__(
+							'WooCommerce %s or greater to be installed and active',
+							'woocommerce-gateway-stripe'
+						),
+						WC_Stripe_UPE_Compatibility::MIN_WC_VERSION
+					),
+				],
+			],
+			function ( $requirement ) {
+				return ! $requirement['is_supported'];
+			}
+		);
+
+		if ( count( $unsatisfied_requirements ) === 0 ) {
+			return;
+		}
+
+		$this->show_current_compatibility_notice( $unsatisfied_requirements );
+	}
+
+	private function get_installed_versions_message( $unsatisfied_requirements ) {
+		return join(
+			__( ' and ', 'woocommerce-gateway-stripe' ),
+			array_map(
+				function ( $requirement ) {
+					return $requirement['name'] . ' ' . $requirement['version'];
+				},
+				$unsatisfied_requirements
+			)
+		);
+	}
+
+	private function get_unsatisfied_requirements_message( $unsatisfied_requirements ) {
+		return join(
+			__( ' and ', 'woocommerce-gateway-stripe' ),
+			array_map(
+				function ( $requirement ) {
+					return $requirement['message'];
+				},
+				$unsatisfied_requirements
+			)
+		);
+	}
+
+	private function show_current_compatibility_notice( $unsatisfied_requirements ) {
+		/*
+		 * The following might be hard to read, but here's what I'm trying to do:
+		 * - If WP and WC are both supported -> nothing to do
+		 * - If WC is not supported -> construct message saying "WooCommerce Stripe requires WooCommerce 5.4 or greater to be installed and active. Your version of WooCommerce [X.X] is no longer supported"
+		 * - If WP is not supported -> construct message saying "WooCommerce Stripe requires WordPress 5.6 or greater. Your version of WordPress [X.X] is no longer supported"
+		 * - If WC & WP are both not supported -> construct message saying "WooCommerce Stripe requires WordPress 5.6 or greater and WooCommerce 5.4 or greater to be installed and active. Your versions of WordPress [X.X] and WooCommerce [X.X] are no longer supported"
+		 */
+		$unsatisfied_requirements_message = $this->get_unsatisfied_requirements_message( $unsatisfied_requirements );
+
+		$unsatisfied_requirements_versions = $this->get_installed_versions_message( $unsatisfied_requirements );
+
+		echo '<div class="error"><p><strong>';
+		echo wp_kses_post(
+			sprintf(
+			/* translators: $1. Minimum WooCommerce and/or WordPress versions. $2. Current WooCommerce and/or versions. $3 Learn more link. */
+				_n(
+					'WooCommerce Stripe requires %1$s. Your version of %2$s is no longer supported.',
+					'WooCommerce Stripe requires %1$s. Your versions of %2$s are no longer supported.',
+					count( $unsatisfied_requirements ),
+					'woocommerce-gateway-stripe'
+				),
+				$unsatisfied_requirements_message,
+				$unsatisfied_requirements_versions
+			)
+		);
+		echo '</strong></p></div>';
+	}
+}
diff --git a/includes/admin/index.html b/includes/admin/index.html
new file mode 100644
index 0000000..64f0111
--- /dev/null
+++ b/includes/admin/index.html
@@ -0,0 +1,38 @@
+<html><head><title> - Revision 2844313: /woocommerce-gateway-stripe/trunk/includes/admin</title></head>
+<body>
+ <h2> - Revision 2844313: /woocommerce-gateway-stripe/trunk/includes/admin</h2>
+ <ul>
+  <li><a href="../">..</a></li>
+  <li><a href="class-wc-rest-stripe-account-controller.php">class-wc-rest-stripe-account-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-account-keys-controller.php">class-wc-rest-stripe-account-keys-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-connection-tokens-controller.php">class-wc-rest-stripe-connection-tokens-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-locations-controller.php">class-wc-rest-stripe-locations-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-orders-controller.php">class-wc-rest-stripe-orders-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-payment-gateway-controller.php">class-wc-rest-stripe-payment-gateway-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-settings-controller.php">class-wc-rest-stripe-settings-controller.php</a></li>
+  <li><a href="class-wc-rest-stripe-tokens-controller.php">class-wc-rest-stripe-tokens-controller.php</a></li>
+  <li><a href="class-wc-stripe-admin-notices.php">class-wc-stripe-admin-notices.php</a></li>
+  <li><a href="class-wc-stripe-inbox-notes.php">class-wc-stripe-inbox-notes.php</a></li>
+  <li><a href="class-wc-stripe-old-settings-upe-toggle-controller.php">class-wc-stripe-old-settings-upe-toggle-controller.php</a></li>
+  <li><a href="class-wc-stripe-payment-gateways-controller.php">class-wc-stripe-payment-gateways-controller.php</a></li>
+  <li><a href="class-wc-stripe-payment-requests-controller.php">class-wc-stripe-payment-requests-controller.php</a></li>
+  <li><a href="class-wc-stripe-privacy.php">class-wc-stripe-privacy.php</a></li>
+  <li><a href="class-wc-stripe-rest-base-controller.php">class-wc-stripe-rest-base-controller.php</a></li>
+  <li><a href="class-wc-stripe-rest-upe-flag-toggle-controller.php">class-wc-stripe-rest-upe-flag-toggle-controller.php</a></li>
+  <li><a href="class-wc-stripe-settings-controller.php">class-wc-stripe-settings-controller.php</a></li>
+  <li><a href="class-wc-stripe-upe-compatibility-controller.php">class-wc-stripe-upe-compatibility-controller.php</a></li>
+  <li><a href="stripe-alipay-settings.php">stripe-alipay-settings.php</a></li>
+  <li><a href="stripe-bancontact-settings.php">stripe-bancontact-settings.php</a></li>
+  <li><a href="stripe-boleto-settings.php">stripe-boleto-settings.php</a></li>
+  <li><a href="stripe-eps-settings.php">stripe-eps-settings.php</a></li>
+  <li><a href="stripe-giropay-settings.php">stripe-giropay-settings.php</a></li>
+  <li><a href="stripe-ideal-settings.php">stripe-ideal-settings.php</a></li>
+  <li><a href="stripe-multibanco-settings.php">stripe-multibanco-settings.php</a></li>
+  <li><a href="stripe-oxxo-settings.php">stripe-oxxo-settings.php</a></li>
+  <li><a href="stripe-p24-settings.php">stripe-p24-settings.php</a></li>
+  <li><a href="stripe-sepa-settings.php">stripe-sepa-settings.php</a></li>
+  <li><a href="stripe-settings.php">stripe-settings.php</a></li>
+  <li><a href="stripe-sofort-settings.php">stripe-sofort-settings.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/admin/stripe-alipay-settings.php b/includes/admin/stripe-alipay-settings.php
new file mode 100644
index 0000000..48e171b
--- /dev/null
+++ b/includes/admin/stripe-alipay-settings.php
@@ -0,0 +1,49 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_alipay_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: China', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => __( '<a href="https://stripe.com/payments/payment-methods-guide#alipay" target="_blank">Payment Method Guide</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe Alipay', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Alipay', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to Alipay.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-bancontact-settings.php b/includes/admin/stripe-bancontact-settings.php
new file mode 100644
index 0000000..3755384
--- /dev/null
+++ b/includes/admin/stripe-bancontact-settings.php
@@ -0,0 +1,49 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_bancontact_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Belgium', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => __( '<a href="https://stripe.com/payments/payment-methods-guide#bancontact" target="_blank">Payment Method Guide</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe Bancontact', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Bancontact', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to Bancontact.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-boleto-settings.php b/includes/admin/stripe-boleto-settings.php
new file mode 100644
index 0000000..8cca638
--- /dev/null
+++ b/includes/admin/stripe-boleto-settings.php
@@ -0,0 +1,57 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_boleto_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Brazil', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => sprintf(
+				/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+				esc_html__( 'Must be activated from your Stripe Dashboard Settings %1$shere%2$s', 'woocommerce-gateway-stripe' ),
+				'<a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">',
+				'</a>'
+			),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe Boleto', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Boleto', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( "You'll be able to download or print the Boleto after checkout.", 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'expiration' => [
+			'title'       => __( 'Expiration', 'woocommerce-gateway-stripe' ),
+			'type'        => 'number',
+			'description' => __( 'This controls the expiration in number of days for the voucher.', 'woocommerce-gateway-stripe' ),
+			'default'     => 3,
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-eps-settings.php b/includes/admin/stripe-eps-settings.php
new file mode 100644
index 0000000..5a0698b
--- /dev/null
+++ b/includes/admin/stripe-eps-settings.php
@@ -0,0 +1,45 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_eps_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Austria', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe EPS', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'EPS', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to EPS.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-giropay-settings.php b/includes/admin/stripe-giropay-settings.php
new file mode 100644
index 0000000..57c0d1d
--- /dev/null
+++ b/includes/admin/stripe-giropay-settings.php
@@ -0,0 +1,49 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_giropay_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Germany', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => __( '<a href="https://stripe.com/payments/payment-methods-guide#giropay" target="_blank">Payment Method Guide</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe giropay', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'giropay', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to giropay.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-ideal-settings.php b/includes/admin/stripe-ideal-settings.php
new file mode 100644
index 0000000..5a43186
--- /dev/null
+++ b/includes/admin/stripe-ideal-settings.php
@@ -0,0 +1,49 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_ideal_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: The Netherlands', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => __( '<a href="https://stripe.com/payments/payment-methods-guide#ideal" target="_blank">Payment Method Guide</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe iDEAL', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'iDEAL', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to iDEAL.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-multibanco-settings.php b/includes/admin/stripe-multibanco-settings.php
new file mode 100644
index 0000000..2b39622
--- /dev/null
+++ b/includes/admin/stripe-multibanco-settings.php
@@ -0,0 +1,45 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_multibanco_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Portugal', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe Multibanco', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Multibanco', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to Multibanco.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-oxxo-settings.php b/includes/admin/stripe-oxxo-settings.php
new file mode 100644
index 0000000..c516f0c
--- /dev/null
+++ b/includes/admin/stripe-oxxo-settings.php
@@ -0,0 +1,59 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_oxxo_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Mexico', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => sprintf(
+				/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+				esc_html__( '%1$sPayment Method Guide%2$s', 'woocommerce-gateway-stripe' ),
+				'<a href="https://stripe.com/payments/payment-methods-guide#oxxo" target="_blank">',
+				'</a>'
+			),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => sprintf(
+			/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
+				esc_html__( 'Must be activated from your Stripe Dashboard Settings %1$shere%2$s', 'woocommerce-gateway-stripe' ),
+				'<a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">',
+				'</a>'
+			),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe OXXO', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'OXXO', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( "You'll be able to download or print the OXXO voucher after checkout.", 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-p24-settings.php b/includes/admin/stripe-p24-settings.php
new file mode 100644
index 0000000..9de838d
--- /dev/null
+++ b/includes/admin/stripe-p24-settings.php
@@ -0,0 +1,45 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_p24_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Poland', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe P24', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Przelewy24 (P24)', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to P24.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-sepa-settings.php b/includes/admin/stripe-sepa-settings.php
new file mode 100644
index 0000000..8101f1f
--- /dev/null
+++ b/includes/admin/stripe-sepa-settings.php
@@ -0,0 +1,49 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_sepa_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: France, Germany, Spain, Belgium, Netherlands, Luxembourg, Italy, Portugal, Austria, Ireland', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => __( '<a href="https://stripe.com/payments/payment-methods-guide#sepa-direct-debit" target="_blank">Payment Method Guide</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe SEPA Direct Debit', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'SEPA Direct Debit', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Mandate Information.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);
diff --git a/includes/admin/stripe-settings.php b/includes/admin/stripe-settings.php
new file mode 100644
index 0000000..6f0525d
--- /dev/null
+++ b/includes/admin/stripe-settings.php
@@ -0,0 +1,296 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+$is_gte_wc6_6 = defined( WC_VERSION ) && version_compare( WC_VERSION, '6.6', '>=' );
+
+$stripe_settings = apply_filters(
+	'wc_stripe_settings',
+	[
+		'enabled'                             => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'                               => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => $is_gte_wc6_6 ? 'safe_text' : 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Credit Card (Stripe)', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'title_upe'                           => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => $is_gte_wc6_6 ? 'safe_text' : 'text',
+			'description' => __( 'This controls the title which the user sees during checkout when multiple payment methods are enabled.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Popular payment methods', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description'                         => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Pay with your credit card via Stripe.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'api_credentials'                     => [
+			'title' => __( 'Stripe Account Keys', 'woocommerce-gateway-stripe' ),
+			'type'  => 'stripe_account_keys',
+		],
+		'testmode'                            => [
+			'title'       => __( 'Test mode', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Test Mode', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => __( 'Place the payment gateway in test mode using test API keys.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'yes',
+			'desc_tip'    => true,
+		],
+		'test_publishable_key'                => [
+			'title'       => __( 'Test Publishable Key', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'Get your API keys from your stripe account. Invalid values will be rejected. Only values starting with "pk_test_" will be saved.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'test_secret_key'                     => [
+			'title'       => __( 'Test Secret Key', 'woocommerce-gateway-stripe' ),
+			'type'        => 'password',
+			'description' => __( 'Get your API keys from your stripe account. Invalid values will be rejected. Only values starting with "sk_test_" or "rk_test_" will be saved.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'publishable_key'                     => [
+			'title'       => __( 'Live Publishable Key', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'Get your API keys from your stripe account. Invalid values will be rejected. Only values starting with "pk_live_" will be saved.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'secret_key'                          => [
+			'title'       => __( 'Live Secret Key', 'woocommerce-gateway-stripe' ),
+			'type'        => 'password',
+			'description' => __( 'Get your API keys from your stripe account. Invalid values will be rejected. Only values starting with "sk_live_" or "rk_live_" will be saved.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'webhook'                             => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+		'test_webhook_secret'                 => [
+			'title'       => __( 'Test Webhook Secret', 'woocommerce-gateway-stripe' ),
+			'type'        => 'password',
+			'description' => __( 'Get your webhook signing secret from the webhooks section in your stripe account.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'webhook_secret'                      => [
+			'title'       => __( 'Webhook Secret', 'woocommerce-gateway-stripe' ),
+			'type'        => 'password',
+			'description' => __( 'Get your webhook signing secret from the webhooks section in your stripe account.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'inline_cc_form'                      => [
+			'title'       => __( 'Inline Credit Card Form', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => __( 'Choose the style you want to show for your credit card form. When unchecked, the credit card form will display separate credit card number field, expiry date field and cvc field.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'no',
+			'desc_tip'    => true,
+		],
+		'statement_descriptor'                => [
+			'title'       => __( 'Statement Descriptor', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'Statement descriptors are limited to 22 characters, cannot use the special characters >, <, ", \, \', *, /, (, ), {, }, and must not consist solely of numbers. This will appear on your customer\'s statement in capital letters.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'short_statement_descriptor'          => [
+			'title'       => __( 'Short Statement Descriptor', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'Shortened version of the statement descriptor in combination with the customer order number.', 'woocommerce-gateway-stripe' ),
+			'default'     => '',
+			'desc_tip'    => true,
+		],
+		'capture'                             => [
+			'title'       => __( 'Capture', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Capture charge immediately', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => __( 'Whether or not to immediately capture the charge. When unchecked, the charge issues an authorization and will need to be captured later. Uncaptured charges expire in 7 days.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'yes',
+			'desc_tip'    => true,
+		],
+		'payment_request'                     => [
+			'title'       => __( 'Payment Request Buttons', 'woocommerce-gateway-stripe' ),
+			'label'       => sprintf(
+				/* translators: 1) br tag 2) Stripe anchor tag 3) Apple anchor tag 4) Stripe dashboard opening anchor tag 5) Stripe dashboard closing anchor tag */
+				__( 'Enable Payment Request Buttons. (Apple Pay/Google Pay) %1$sBy using Apple Pay, you agree to %2$s and %3$s\'s terms of service. (Apple Pay domain verification is performed automatically in live mode; configuration can be found on the %4$sStripe dashboard%5$s.)', 'woocommerce-gateway-stripe' ),
+				'<br />',
+				'<a href="https://stripe.com/apple-pay/legal" target="_blank">Stripe</a>',
+				'<a href="https://developer.apple.com/apple-pay/acceptable-use-guidelines-for-websites/" target="_blank">Apple</a>',
+				'<a href="https://dashboard.stripe.com/settings/payments/apple_pay" target="_blank">',
+				'</a>'
+			),
+			'type'        => 'checkbox',
+			'description' => __( 'If enabled, users will be able to pay using Apple Pay or Chrome Payment Request if supported by the browser.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'yes',
+			'desc_tip'    => true,
+		],
+		'payment_request_button_type'         => [
+			'title'       => __( 'Payment Request Button Type', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Button Type', 'woocommerce-gateway-stripe' ),
+			'type'        => 'select',
+			'description' => __( 'Select the button type you would like to show.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'buy',
+			'desc_tip'    => true,
+			'options'     => [
+				'default' => __( 'Default', 'woocommerce-gateway-stripe' ),
+				'buy'     => __( 'Buy', 'woocommerce-gateway-stripe' ),
+				'donate'  => __( 'Donate', 'woocommerce-gateway-stripe' ),
+				'branded' => __( 'Branded', 'woocommerce-gateway-stripe' ),
+				'custom'  => __( 'Custom', 'woocommerce-gateway-stripe' ),
+			],
+		],
+		'payment_request_button_theme'        => [
+			'title'       => __( 'Payment Request Button Theme', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Button Theme', 'woocommerce-gateway-stripe' ),
+			'type'        => 'select',
+			'description' => __( 'Select the button theme you would like to show.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'dark',
+			'desc_tip'    => true,
+			'options'     => [
+				'dark'          => __( 'Dark', 'woocommerce-gateway-stripe' ),
+				'light'         => __( 'Light', 'woocommerce-gateway-stripe' ),
+				'light-outline' => __( 'Light-Outline', 'woocommerce-gateway-stripe' ),
+			],
+		],
+		'payment_request_button_height'       => [
+			'title'       => __( 'Payment Request Button Height', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Button Height', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'Enter the height you would like the button to be in pixels. Width will always be 100%.', 'woocommerce-gateway-stripe' ),
+			'default'     => '44',
+			'desc_tip'    => true,
+		],
+		'payment_request_button_label'        => [
+			'title'       => __( 'Payment Request Button Label', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Button Label', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'Enter the custom text you would like the button to have.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Buy now', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'payment_request_button_branded_type' => [
+			'title'       => __( 'Payment Request Branded Button Label Format', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Branded Button Label Format', 'woocommerce-gateway-stripe' ),
+			'type'        => 'select',
+			'description' => __( 'Select the branded button label format.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'long',
+			'desc_tip'    => true,
+			'options'     => [
+				'short' => __( 'Logo only', 'woocommerce-gateway-stripe' ),
+				'long'  => __( 'Text and logo', 'woocommerce-gateway-stripe' ),
+			],
+		],
+		'payment_request_button_locations'    => [
+			'title'             => __( 'Payment Request Button Locations', 'woocommerce-gateway-stripe' ),
+			'type'              => 'multiselect',
+			'description'       => __( 'Select where you would like Payment Request Buttons to be displayed', 'woocommerce-gateway-stripe' ),
+			'desc_tip'          => true,
+			'class'             => 'wc-enhanced-select',
+			'options'           => [
+				'product'  => __( 'Product', 'woocommerce-gateway-stripe' ),
+				'cart'     => __( 'Cart', 'woocommerce-gateway-stripe' ),
+				'checkout' => __( 'Checkout', 'woocommerce-gateway-stripe' ),
+			],
+			'default'           => [ 'product', 'cart' ],
+			'custom_attributes' => [
+				'data-placeholder' => __( 'Select pages', 'woocommerce-gateway-stripe' ),
+			],
+		],
+		'payment_request_button_size'         => [
+			'title'       => __( 'Payment Request Button Size', 'woocommerce-gateway-stripe' ),
+			'type'        => 'select',
+			'description' => __( 'Select the size of the button.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'default',
+			'desc_tip'    => true,
+			'options'     => [
+				'default' => __( 'Default (40px)', 'woocommerce-gateway-stripe' ),
+				'medium'  => __( 'Medium (48px)', 'woocommerce-gateway-stripe' ),
+				'large'   => __( 'Large (56px)', 'woocommerce-gateway-stripe' ),
+			],
+		],
+		'saved_cards'                         => [
+			'title'       => __( 'Saved Cards', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Payment via Saved Cards', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => __( 'If enabled, users will be able to pay with a saved card during checkout. Card details are saved on Stripe servers, not on your store.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'yes',
+			'desc_tip'    => true,
+		],
+		'logging'                             => [
+			'title'       => __( 'Logging', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Log debug messages', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => __( 'Save debug messages to the WooCommerce System Status log.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'no',
+			'desc_tip'    => true,
+		],
+	]
+);
+
+if ( WC_Stripe_Feature_Flags::is_upe_preview_enabled() ) {
+	// in the new settings, "checkout" is going to be enabled by default (if it is a new WCStripe installation).
+	$stripe_settings['payment_request_button_locations']['default'][] = 'checkout';
+
+	// no longer needed in the new settings.
+	unset( $stripe_settings['payment_request_button_branded_type'] );
+	unset( $stripe_settings['payment_request_button_height'] );
+	unset( $stripe_settings['payment_request_button_label'] );
+	// injecting some of the new options.
+	$stripe_settings['payment_request_button_type']['options']['default'] = __( 'Only icon', 'woocommerce-gateway-stripe' );
+	$stripe_settings['payment_request_button_type']['options']['book']    = __( 'Book', 'woocommerce-gateway-stripe' );
+	// no longer valid options.
+	unset( $stripe_settings['payment_request_button_type']['options']['branded'] );
+	unset( $stripe_settings['payment_request_button_type']['options']['custom'] );
+} else {
+	unset( $stripe_settings['payment_request_button_size'] );
+}
+
+if ( WC_Stripe_Feature_Flags::is_upe_preview_enabled() ) {
+	$upe_settings = [
+		WC_Stripe_Feature_Flags::UPE_CHECKOUT_FEATURE_ATTRIBUTE_NAME => [
+			'title'       => __( 'New checkout experience', 'woocommerce-gateway-stripe' ),
+			'label'       => sprintf(
+				/* translators: 1) br tag 2) Stripe anchor tag 3) Apple anchor tag 4) Stripe dashboard opening anchor tag 5) Stripe dashboard closing anchor tag */
+				__( 'Try the new payment experience (Early access) %1$sGet early access to a new, smarter payment experience on checkout and let us know what you think by %2$s. We recommend this feature for experienced merchants as the functionality is currently limited. %3$s', 'woocommerce-gateway-stripe' ),
+				'<br />',
+				'<a href="https://woocommerce.survey.fm/woocommerce-stripe-upe-opt-out-survey" target="_blank">submitting your feedback</a>',
+				'<a href="https://woocommerce.com/document/stripe/#new-checkout-experience" target="_blank">Learn more</a>'
+			),
+			'type'        => 'checkbox',
+			'description' => __( 'New checkout experience allows you to manage all payment methods on one screen and display them to customers based on their currency and location.', 'woocommerce-gateway-stripe' ),
+			'default'     => 'no',
+			'desc_tip'    => true,
+		],
+	];
+	if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
+		// This adds the payment method section
+		$upe_settings['upe_checkout_experience_accepted_payments'] = [
+			'title'   => __( 'Payments accepted on checkout (Early access)', 'woocommerce-gateway-stripe' ),
+			'type'    => 'upe_checkout_experience_accepted_payments',
+			'default' => [ 'card' ],
+		];
+	}
+	// Insert UPE options below the 'logging' setting.
+	$stripe_settings = array_merge( $stripe_settings, $upe_settings );
+}
+
+return apply_filters(
+	'wc_stripe_settings',
+	$stripe_settings
+);
diff --git a/includes/admin/stripe-sofort-settings.php b/includes/admin/stripe-sofort-settings.php
new file mode 100644
index 0000000..d103fde
--- /dev/null
+++ b/includes/admin/stripe-sofort-settings.php
@@ -0,0 +1,49 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+return apply_filters(
+	'wc_stripe_sofort_settings',
+	[
+		'geo_target'  => [
+			'description' => __( 'Customer Geography: Germany, Austria', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'guide'       => [
+			'description' => __( '<a href="https://stripe.com/payments/payment-methods-guide#sofort" target="_blank">Payment Method Guide</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'activation'  => [
+			'description' => __( 'Must be activated from your Stripe Dashboard Settings <a href="https://dashboard.stripe.com/account/payments/settings" target="_blank">here</a>', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+		],
+		'enabled'     => [
+			'title'       => __( 'Enable/Disable', 'woocommerce-gateway-stripe' ),
+			'label'       => __( 'Enable Stripe Sofort', 'woocommerce-gateway-stripe' ),
+			'type'        => 'checkbox',
+			'description' => '',
+			'default'     => 'no',
+		],
+		'title'       => [
+			'title'       => __( 'Title', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'Sofort', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'description' => [
+			'title'       => __( 'Description', 'woocommerce-gateway-stripe' ),
+			'type'        => 'text',
+			'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-gateway-stripe' ),
+			'default'     => __( 'You will be redirected to Sofort.', 'woocommerce-gateway-stripe' ),
+			'desc_tip'    => true,
+		],
+		'webhook'     => [
+			'title'       => __( 'Webhook Endpoints', 'woocommerce-gateway-stripe' ),
+			'type'        => 'title',
+			/* translators: webhook URL */
+			'description' => $this->display_admin_settings_webhook_description(),
+		],
+	]
+);