init
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* Overwrites the default payment settings sections in WooCommerce
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC_Payments_Admin_Sections_Overwrite Class.
|
||||
*/
|
||||
class WC_Payments_Admin_Sections_Overwrite {
|
||||
/**
|
||||
* WC_Payments_Account instance to get information about the account.
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC_Payments_Admin_Sections_Overwrite constructor.
|
||||
*
|
||||
* @param WC_Payments_Account $account WC_Payments_Account instance.
|
||||
*/
|
||||
public function __construct( WC_Payments_Account $account ) {
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_filter( 'woocommerce_get_sections_checkout', [ $this, 'add_checkout_sections' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an "all payment methods" and a "woopayments" section to the gateways settings page
|
||||
*
|
||||
* @param array $default_sections the sections for the payment gateways tab.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_checkout_sections( array $default_sections ): array {
|
||||
$sections_to_render = [];
|
||||
$sections_to_render['woocommerce_payments'] = 'WooPayments';
|
||||
$sections_to_render[''] = __( 'All payment methods', 'woocommerce-payments' );
|
||||
|
||||
return $sections_to_render;
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Admin_Settings
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
/**
|
||||
* WC_Payments_Admin_Settings class.
|
||||
*/
|
||||
class WC_Payments_Admin_Settings {
|
||||
|
||||
/**
|
||||
* WC_Payment_Gateway_WCPay.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* Set of parameters to build the URL to the gateway's settings page.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $settings_url_params = [
|
||||
'page' => 'wc-settings',
|
||||
'tab' => 'checkout',
|
||||
'section' => WC_Payment_Gateway_WCPay::GATEWAY_ID,
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize class actions.
|
||||
*
|
||||
* @param WC_Payment_Gateway_WCPay $gateway Payment Gateway.
|
||||
*/
|
||||
public function __construct( WC_Payment_Gateway_WCPay $gateway ) {
|
||||
$this->gateway = $gateway;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_test_mode_notice' ] );
|
||||
add_filter( 'plugin_action_links_' . plugin_basename( WCPAY_PLUGIN_FILE ), [ $this, 'add_plugin_links' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add notice explaining test mode when it's enabled.
|
||||
*/
|
||||
public function display_test_mode_notice() {
|
||||
if ( WC_Payments::mode()->is_test() ) {
|
||||
?>
|
||||
<div id="wcpay-test-mode-notice" class="notice notice-warning">
|
||||
<p>
|
||||
<b><?php esc_html_e( 'Test mode active: ', 'woocommerce-payments' ); ?></b>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: WooPayments */
|
||||
esc_html__( "All transactions are simulated. Customers can't make real purchases through %s.", 'woocommerce-payments' ),
|
||||
'WooPayments'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to the plugin's row in the "Plugins" Wp-Admin page.
|
||||
*
|
||||
* @see https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
|
||||
* @param array $links The existing list of links that will be rendered.
|
||||
* @return array The list of links that will be rendered, after adding some links specific to this plugin.
|
||||
*/
|
||||
public function add_plugin_links( $links ) {
|
||||
$plugin_links = [
|
||||
'<a href="' . esc_attr( self::get_settings_url() ) . '">' . esc_html__( 'Settings', 'woocommerce-payments' ) . '</a>',
|
||||
];
|
||||
|
||||
return array_merge( $plugin_links, $links );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current page is the WooPayments settings page.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_current_page_settings() {
|
||||
return count( self::$settings_url_params ) === count( array_intersect_assoc( $_GET, self::$settings_url_params ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the configuration screen for this gateway, for use in internal links.
|
||||
*
|
||||
* @param array $query_args Optional additional query args to append to the URL.
|
||||
*
|
||||
* @return string URL of the configuration screen for this gateway
|
||||
*/
|
||||
public static function get_settings_url( $query_args = [] ) {
|
||||
return admin_url( add_query_arg( array_merge( self::$settings_url_params, $query_args ), 'admin.php' ) ); // nosemgrep: audit.php.wp.security.xss.query-arg -- constant string is passed in.
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_REST_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for transactions.
|
||||
*/
|
||||
class WC_Payments_REST_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v3';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
protected $api_client;
|
||||
|
||||
/**
|
||||
* WC_Payments_REST_Controller constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client - WooCommerce Payments API client.
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $api_client ) {
|
||||
$this->api_client = $api_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards request to API client with taking care of API_Exception.
|
||||
*
|
||||
* @param string $api_method - API method name.
|
||||
* @param array $args - API method args.
|
||||
*
|
||||
* @return WP_Error|mixed - Method result of WP_Error in case of API_Exception.
|
||||
*/
|
||||
public function forward_request( $api_method, $args ) {
|
||||
try {
|
||||
$response = call_user_func_array( [ $this->api_client, $api_method ], $args );
|
||||
} catch ( API_Exception $e ) {
|
||||
$response = new WP_Error( $e->get_error_code(), $e->getMessage() );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify access.
|
||||
*
|
||||
* Override this method if custom permissions required.
|
||||
*/
|
||||
public function check_permission() {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Accounts_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for account details and status.
|
||||
*/
|
||||
class WC_REST_Payments_Accounts_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/accounts';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/payments/accounts',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_account_data' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account details via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_account_data( $request ) {
|
||||
$account = WC_Payments::get_account_service()->get_cached_account_data();
|
||||
if ( [] === $account ) {
|
||||
$default_currency = get_woocommerce_currency();
|
||||
$status = WC_Payments_Account::is_on_boarding_disabled() ? 'ONBOARDING_DISABLED' : 'NOACCOUNT';
|
||||
$account = [
|
||||
'card_present_eligible' => false,
|
||||
'country' => WC()->countries->get_base_country(),
|
||||
'current_deadline' => null,
|
||||
'has_overdue_requirements' => false,
|
||||
'has_pending_requirements' => false,
|
||||
'statement_descriptor' => '',
|
||||
'status' => $status,
|
||||
'store_currencies' => [
|
||||
'default' => $default_currency,
|
||||
'supported' => [
|
||||
$default_currency,
|
||||
],
|
||||
],
|
||||
'customer_currencies' => [
|
||||
'supported' => [
|
||||
$default_currency,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ( false !== $account ) {
|
||||
// Add extra properties to account if necessary.
|
||||
$account['card_present_eligible'] = false;
|
||||
$account['test_mode'] = WC_Payments::mode()->is_test();
|
||||
$account['test_mode_onboarding'] = WC_Payments::mode()->is_test_mode_onboarding();
|
||||
}
|
||||
|
||||
return rest_ensure_response( $account );
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Authorizations_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request;
|
||||
use WCPay\Core\Server\Request\List_Authorizations;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for authorizations.
|
||||
*/
|
||||
class WC_REST_Payments_Authorizations_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/authorizations';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_authorizations' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_authorizations_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<payment_intent_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_authorization' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authorizations to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_authorizations( WP_REST_Request $request ) {
|
||||
$wcpay_request = List_Authorizations::from_rest_request( $request );
|
||||
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authorization to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_authorization( WP_REST_Request $request ) {
|
||||
$payment_intent_id = $request->get_param( 'payment_intent_id' );
|
||||
$request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API, $payment_intent_id );
|
||||
$request->assign_hook( 'wcpay_get_authorization_request' );
|
||||
return $request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authorizations summary to respond with via API.
|
||||
*/
|
||||
public function get_authorizations_summary() {
|
||||
$request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API . '/summary' );
|
||||
$request->assign_hook( 'wc_pay_get_authorizations_summary' );
|
||||
return $request->handle_rest_request();
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Capital_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for Capital loans functionality.
|
||||
*/
|
||||
class WC_REST_Payments_Capital_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/capital';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/active_loan_summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_active_loan_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/loans',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_loans' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the summary of the active Capital loan.
|
||||
*/
|
||||
public function get_active_loan_summary() {
|
||||
$request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/active_loan_summary' );
|
||||
$request->assign_hook( 'wcpay_get_active_loan_summary_request' );
|
||||
return $request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the past and present Capital loans.
|
||||
*/
|
||||
public function get_loans() {
|
||||
$request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/loans' );
|
||||
$request->assign_hook( 'wcpay_get_loans_request' );
|
||||
return $request->handle_rest_request();
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Charges_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Get_Charge;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for charges.
|
||||
*/
|
||||
class WC_REST_Payments_Charges_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/charges';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<charge_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_charge' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/order/(?P<order_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'generate_charge_from_order' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve charge to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_charge( $request ) {
|
||||
$charge_id = $request->get_param( 'charge_id' );
|
||||
|
||||
try {
|
||||
$wcpay_request = Get_Charge::create( $charge_id );
|
||||
$charge = $wcpay_request->send();
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( 'wcpay_get_charge', $e->getMessage() ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $charge );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a charge-like object from an order.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function generate_charge_from_order( $request ) {
|
||||
$order_id = $request['order_id'];
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( false === $order ) {
|
||||
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
$currency = $order->get_currency();
|
||||
$amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency );
|
||||
$billing_details = WC_Payments::get_order_service()->get_billing_data_from_order( $order ); // TODO: Inject order_service after #7464 is fixed.
|
||||
$date_created = $order->get_date_created();
|
||||
$intent_id = $order->get_meta( '_intent_id' );
|
||||
$intent_status = $order->get_meta( '_intent_status' );
|
||||
|
||||
$charge = [
|
||||
'id' => $order->get_id(),
|
||||
'amount' => $amount,
|
||||
'amount_captured' => 0,
|
||||
'amount_refunded' => 0,
|
||||
'application_fee_amount' => 0,
|
||||
'balance_transaction' => [
|
||||
'currency' => $currency,
|
||||
'amount' => $amount,
|
||||
'fee' => 0,
|
||||
],
|
||||
'billing_details' => $billing_details,
|
||||
'created' => $date_created ? $date_created->getTimestamp() : null,
|
||||
'currency' => $currency,
|
||||
'disputed' => false,
|
||||
'outcome' => false,
|
||||
'order' => $this->api_client->build_order_info( $order ),
|
||||
'paid' => false,
|
||||
'paydown' => null,
|
||||
'payment_intent' => ! empty( $intent_id ) ? $intent_id : null,
|
||||
'payment_method_details' => [
|
||||
'card' => [
|
||||
'country' => $order->get_billing_country(),
|
||||
'checks' => [],
|
||||
'network' => '',
|
||||
],
|
||||
'type' => 'card',
|
||||
],
|
||||
'refunded' => false,
|
||||
'refunds' => null,
|
||||
'status' => ! empty( $intent_status ) ? $intent_status : $order->get_status(),
|
||||
];
|
||||
|
||||
$charge = $this->api_client->add_formatted_address_to_charge_object( $charge );
|
||||
|
||||
return rest_ensure_response( $charge );
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Connection_Tokens_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for terminal tokens.
|
||||
*/
|
||||
class WC_REST_Payments_Connection_Tokens_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/connection_tokens';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/payments/connection_tokens',
|
||||
[
|
||||
'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 = $this->forward_request( 'create_token', [ $request ] );
|
||||
|
||||
// As an aid to mobile clients, tuck in the test_mode flag in the response returned to the request.
|
||||
if ( is_a( $response, 'WP_REST_Response' ) ) {
|
||||
if ( property_exists( $response, 'data' ) ) {
|
||||
if ( is_array( $response->data ) ) {
|
||||
$response->data['test_mode'] = WC_Payments::mode()->is_test();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Customer_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for customers.
|
||||
*/
|
||||
class WC_REST_Payments_Customer_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Onboarding Service.
|
||||
*
|
||||
* @var WC_Payments_Customer_Service
|
||||
*/
|
||||
protected $customer_service;
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/customers';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
|
||||
* @param WC_Payments_Customer_Service $customer_service Token service.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $api_client,
|
||||
WC_Payments_Customer_Service $customer_service
|
||||
) {
|
||||
parent::__construct( $api_client );
|
||||
$this->customer_service = $customer_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<customer_id>\w+)/payment_methods',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_customer_payment_methods' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
],
|
||||
'schema' => [ $this, 'get_item_schema' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transaction to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_customer_payment_methods( $request ) {
|
||||
$customer_id = $request->get_param( 'customer_id' );
|
||||
$payment_methods_types = WC_Payments::get_gateway()->get_upe_enabled_payment_method_ids() ?? [];
|
||||
$payment_methods = [];
|
||||
|
||||
// Perhaps we can fetch it directly from server and avoid looping to get payment methods from cache.
|
||||
foreach ( $payment_methods_types as $type ) {
|
||||
try {
|
||||
$payment_methods[] = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type );
|
||||
} catch ( API_Exception $e ) {
|
||||
wp_send_json_error(
|
||||
wp_strip_all_tags( $e->getMessage() ),
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$payment_methods = array_merge( ...$payment_methods );
|
||||
$data = [];
|
||||
foreach ( $payment_methods as $payment_method ) {
|
||||
$response = $this->prepare_item_for_response( $payment_method, $request );
|
||||
$data[] = $this->prepare_response_for_collection( $response );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare each item for response.
|
||||
*
|
||||
* @param array|mixed $item Item to prepare.
|
||||
* @param WP_REST_Request $request Request instance.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function prepare_item_for_response( $item, $request ) {
|
||||
|
||||
$prepared_item = [];
|
||||
|
||||
$prepared_item['id'] = $item['id'];
|
||||
$prepared_item['type'] = $item['type'];
|
||||
$prepared_item['billing_details'] = $item['billing_details'];
|
||||
if ( array_key_exists( 'card', $item ) ) {
|
||||
$prepared_item['card'] = [
|
||||
'brand' => $item['card']['brand'],
|
||||
'last4' => $item['card']['last4'],
|
||||
'exp_month' => $item['card']['exp_month'],
|
||||
'exp_year' => $item['card']['exp_year'],
|
||||
];
|
||||
}
|
||||
if ( array_key_exists( 'card', $item ) ) {
|
||||
$prepared_item['card'] = [
|
||||
'brand' => $item['card']['brand'],
|
||||
'last4' => $item['card']['last4'],
|
||||
'exp_month' => $item['card']['exp_month'],
|
||||
'exp_year' => $item['card']['exp_year'],
|
||||
];
|
||||
} elseif ( array_key_exists( 'sepa_debit', $item ) ) {
|
||||
$prepared_item['sepa_debit'] = [
|
||||
'last4' => $item['sepa_debit']['last4'],
|
||||
];
|
||||
} elseif ( array_key_exists( 'link', $item ) ) {
|
||||
$prepared_item['link'] = [
|
||||
'email' => $item['link']['email'],
|
||||
];
|
||||
}
|
||||
|
||||
$context = $request['context'] ?? 'view';
|
||||
$prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request );
|
||||
$prepared_item = $this->filter_response_by_context( $prepared_item, $context );
|
||||
|
||||
return rest_ensure_response( $prepared_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Item schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return [
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'payment_method',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => [
|
||||
'description' => __( 'ID for the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'type' => [
|
||||
'description' => __( 'Type of the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'enum' => [ 'card', 'sepa_debit', 'link' ],
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'billing_details' => [
|
||||
'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'address' => [
|
||||
'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'city' => [
|
||||
'description' => __( 'City of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'country' => [
|
||||
'description' => __( 'Country of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'line1' => [
|
||||
'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'line2' => [
|
||||
'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'postal_code' => [
|
||||
'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'state' => [
|
||||
'description' => __( 'State of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
'email' => [
|
||||
'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'format' => 'email',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'name' => [
|
||||
'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'phone' => [
|
||||
'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
'card' => [
|
||||
'description' => __( 'Card details for the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'brand' => [
|
||||
'description' => __( 'Brand of the card.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'last4' => [
|
||||
'description' => __( 'Last 4 digits of the card.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'exp_month' => [
|
||||
'description' => __( 'Expiration month of the card.', 'woocommerce-payments' ),
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'exp_year' => [
|
||||
'description' => __( 'Expiration year of the card.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
'sepa_debit' => [
|
||||
'description' => __( 'SEPA Debit details for the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'last4' => [
|
||||
'description' => __( 'Last 4 digits of the SEPA Debit.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
'link' => [
|
||||
'description' => __( 'Link details for the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'email' => [
|
||||
'description' => __( 'Email associated with the link.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'format' => 'email',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Deposits_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request;
|
||||
use WCPay\Core\Server\Request\List_Deposits;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for deposits.
|
||||
*/
|
||||
class WC_REST_Payments_Deposits_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/deposits';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_deposits' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_deposits_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/download',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_deposits_export' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<deposit_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_deposit' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/overview-all',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_all_deposits_overviews' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'manual_deposit' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve deposits to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_deposits( $request ) {
|
||||
$wcpay_request = List_Deposits::from_rest_request( $request );
|
||||
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve deposits summary to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_deposits_summary( $request ) {
|
||||
$filters = $this->get_deposits_filters( $request );
|
||||
return $this->forward_request( 'get_deposits_summary', [ $filters ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an overview of all deposits from the API.
|
||||
*/
|
||||
public function get_all_deposits_overviews() {
|
||||
$request = Request::get( WC_Payments_API_Client::DEPOSITS_API . '/overview-all' );
|
||||
$request->assign_hook( 'wcpay_get_all_deposits_overviews' );
|
||||
return $request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve deposit to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_deposit( $request ) {
|
||||
$deposit_id = $request->get_param( 'deposit_id' );
|
||||
$wcpay_request = Request::get( WC_Payments_API_Client::DEPOSITS_API, $deposit_id );
|
||||
$wcpay_request->assign_hook( 'wcpay_get_deposit' );
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate deposits export via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_deposits_export( $request ) {
|
||||
$user_email = $request->get_param( 'user_email' );
|
||||
$locale = $request->get_param( 'locale' );
|
||||
$filters = $this->get_deposits_filters( $request );
|
||||
|
||||
return $this->forward_request( 'get_deposits_export', [ $filters, $user_email, $locale ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract deposits filters from request
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
private function get_deposits_filters( $request ) {
|
||||
return array_filter(
|
||||
[
|
||||
'match' => $request->get_param( 'match' ),
|
||||
'store_currency_is' => $request->get_param( 'store_currency_is' ),
|
||||
'date_before' => $request->get_param( 'date_before' ),
|
||||
'date_after' => $request->get_param( 'date_after' ),
|
||||
'date_between' => $request->get_param( 'date_between' ),
|
||||
'status_is' => $request->get_param( 'status_is' ),
|
||||
'status_is_not' => $request->get_param( 'status_is_not' ),
|
||||
],
|
||||
static function ( $filter ) {
|
||||
return null !== $filter;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a manual deposit.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function manual_deposit( $request ) {
|
||||
$params = $request->get_params();
|
||||
return $this->forward_request( 'manual_deposit', [ $params['type'], $params['currency'] ] );
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Disputes_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\List_Disputes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for disputes.
|
||||
*/
|
||||
class WC_REST_Payments_Disputes_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/disputes';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_disputes' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_disputes_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/download',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_disputes_export' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<dispute_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_dispute' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<dispute_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'update_dispute' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<dispute_id>\w+)/close',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'close_dispute' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve disputes to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_disputes( WP_REST_Request $request ) {
|
||||
$wcpay_request = List_Disputes::from_rest_request( $request );
|
||||
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions summary to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_disputes_summary( WP_REST_Request $request ) {
|
||||
$filters = $this->get_disputes_filters( $request );
|
||||
return $this->forward_request( 'get_disputes_summary', [ $filters ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve dispute to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_dispute( $request ) {
|
||||
$dispute_id = $request->get_param( 'dispute_id' );
|
||||
return $this->forward_request( 'get_dispute', [ $dispute_id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update and respond with dispute via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function update_dispute( $request ) {
|
||||
$params = $request->get_params();
|
||||
return $this->forward_request(
|
||||
'update_dispute',
|
||||
[
|
||||
$params['dispute_id'],
|
||||
$params['evidence'],
|
||||
$params['submit'],
|
||||
$params['metadata'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and respond with dispute via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function close_dispute( $request ) {
|
||||
$dispute_id = $request->get_param( 'dispute_id' );
|
||||
return $this->forward_request( 'close_dispute', [ $dispute_id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate disputes export via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_disputes_export( $request ) {
|
||||
$user_email = $request->get_param( 'user_email' );
|
||||
$locale = $request->get_param( 'locale' );
|
||||
$filters = $this->get_disputes_filters( $request );
|
||||
|
||||
return $this->forward_request( 'get_disputes_export', [ $filters, $user_email, $locale ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract disputes filters from request
|
||||
* The reason to map the filter properties is to keep consitency between the front-end models.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
private function get_disputes_filters( $request ) {
|
||||
return array_filter(
|
||||
[
|
||||
'match' => $request->get_param( 'match' ),
|
||||
'currency_is' => $request->get_param( 'store_currency_is' ),
|
||||
'created_before' => $request->get_param( 'date_before' ),
|
||||
'created_after' => $request->get_param( 'date_after' ),
|
||||
'created_between' => $request->get_param( 'date_between' ),
|
||||
'search' => $request->get_param( 'search' ),
|
||||
'status_is' => $request->get_param( 'status_is' ),
|
||||
'status_is_not' => $request->get_param( 'status_is_not' ),
|
||||
],
|
||||
static function ( $filter ) {
|
||||
return null !== $filter;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Documents_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\List_Documents;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for documents.
|
||||
*/
|
||||
class WC_REST_Payments_Documents_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/documents';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_documents' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_documents_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<document_id>[\w-]+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_document' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve documents to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_documents( $request ) {
|
||||
$wcpay_request = List_Documents::from_rest_request( $request );
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve documents summary to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_documents_summary( $request ) {
|
||||
$filters = $this->get_documents_filters( $request );
|
||||
return $this->forward_request( 'get_documents_summary', [ $filters ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and serve a document for API requests.
|
||||
* This method serves the document directly and halts execution, skipping the REST return
|
||||
* and preventing additional data to be sent.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_document( $request ) {
|
||||
$response = [];
|
||||
$document_id = $request->get_param( 'document_id' );
|
||||
|
||||
try {
|
||||
$response = $this->api_client->get_document( $document_id );
|
||||
} catch ( API_Exception $e ) {
|
||||
$message = sprintf(
|
||||
/* translators: %1: The document ID. %2: The error message.*/
|
||||
esc_html__( 'There was an error accessing document %1$s. %2$s', 'woocommerce-payments' ),
|
||||
$document_id,
|
||||
$e->getMessage()
|
||||
);
|
||||
wp_die( esc_html( $message ), '', (int) $e->get_http_code() );
|
||||
}
|
||||
|
||||
// WooCommerce core only includes Tracks in admin, not the REST API, so we need to use this wc_admin method
|
||||
// that includes WC_Tracks in case it's not loaded.
|
||||
if ( function_exists( 'wc_admin_record_tracks_event' ) ) {
|
||||
wc_admin_record_tracks_event(
|
||||
'wcpay_document_downloaded',
|
||||
[
|
||||
'document_id' => $document_id,
|
||||
'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Set the headers to match what was returned from the server.
|
||||
if ( ! headers_sent() ) {
|
||||
nocache_headers();
|
||||
status_header( $response['response']['code'], $response['response']['message'] ?? '' );
|
||||
header( 'Content-Type: ' . $response['headers']['content-type'] );
|
||||
header( 'Content-Disposition: ' . $response['headers']['content-disposition'] ?? '' );
|
||||
}
|
||||
|
||||
// We should output the server's file without escaping.
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $response['body'];
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract documents filters from request
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
private function get_documents_filters( $request ) {
|
||||
return array_filter(
|
||||
[
|
||||
'match' => $request->get_param( 'match' ),
|
||||
'date_before' => $request->get_param( 'date_before' ),
|
||||
'date_after' => $request->get_param( 'date_after' ),
|
||||
'date_between' => $request->get_param( 'date_between' ),
|
||||
'type_is' => $request->get_param( 'type_is' ),
|
||||
'type_is_not' => $request->get_param( 'type_is_not' ),
|
||||
],
|
||||
static function ( $filter ) {
|
||||
return null !== $filter;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Files_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for files.
|
||||
*/
|
||||
class WC_REST_Payments_Files_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/file';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'upload_file' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<file_id>\w+)/details',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_file_detail' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<file_id>\w+)/content',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_file_content' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<file_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_file' ],
|
||||
'permission_callback' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file and respond with file object via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function upload_file( $request ) {
|
||||
return $this->forward_request( 'upload_file', [ $request ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a file content via API.
|
||||
*
|
||||
* @param WP_REST_Request $request - request object.
|
||||
*
|
||||
* @return WP_Error|WP_HTTP_Response
|
||||
*/
|
||||
public function get_file( WP_REST_Request $request ) {
|
||||
$file_id = $request->get_param( 'file_id' );
|
||||
$as_account = (bool) $request->get_param( 'as_account' );
|
||||
|
||||
$file_service = new WC_Payments_File_Service();
|
||||
$purpose = get_transient( WC_Payments_File_Service::CACHE_KEY_PREFIX_PURPOSE . $file_id . '_' . ( $as_account ? '1' : '0' ) );
|
||||
|
||||
if ( ! $purpose ) {
|
||||
$file = $this->forward_request( 'get_file', [ $file_id, $as_account ] );
|
||||
|
||||
if ( is_wp_error( $file ) ) {
|
||||
return $this->file_error_response( $file );
|
||||
}
|
||||
$purpose = $file->get_data()['purpose'];
|
||||
set_transient( WC_Payments_File_Service::CACHE_KEY_PREFIX_PURPOSE . $file_id, $purpose, WC_Payments_File_Service::CACHE_PERIOD );
|
||||
}
|
||||
|
||||
if ( ! $file_service->is_file_public( $purpose ) && ! $this->check_permission() ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'Sorry, you are not allowed to do that.', 'woocommerce-payments' ),
|
||||
[ 'status' => rest_authorization_required_code() ]
|
||||
);
|
||||
}
|
||||
|
||||
$result = $this->forward_request( 'get_file_contents', [ $file_id, $as_account ] );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->file_error_response( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* WP_REST_Server will convert the response data to JSON prior to output it.
|
||||
* Using this filter to prevent it, and output the data from WP_HTTP_Response instead.
|
||||
*/
|
||||
add_filter(
|
||||
'rest_pre_serve_request',
|
||||
function ( bool $served, WP_HTTP_Response $response ): bool {
|
||||
echo $response->get_data(); // @codingStandardsIgnoreLine
|
||||
return true;
|
||||
},
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
return new WP_HTTP_Response(
|
||||
base64_decode( $result->get_data()['file_content'] ), // @codingStandardsIgnoreLine
|
||||
200,
|
||||
[
|
||||
'Content-Type' => $result->get_data()['content_type'],
|
||||
'Content-Disposition' => 'inline',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve file details via the API.
|
||||
*
|
||||
* Example response:
|
||||
* {
|
||||
* "id": "file_1Np1S5J5cIRIG92xknlr0iND",
|
||||
* "object": "file",
|
||||
* "created": 1694405421,
|
||||
* "expires_at": 1717733421,
|
||||
* "filename": "Screenshot 2023-09-04 at 5.08.31\u202fPM.png",
|
||||
* "purpose": "dispute_evidence",
|
||||
* "size": 21444,
|
||||
* "title": null,
|
||||
* "type": "png",
|
||||
* }
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
public function get_file_detail( WP_REST_Request $request ) {
|
||||
$file_id = $request->get_param( 'file_id' );
|
||||
$as_account = (bool) $request->get_param( 'as_account' );
|
||||
|
||||
return $this->forward_request( 'get_file', [ $file_id, $as_account ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve file contents via the API as a base64 encoded string.
|
||||
*
|
||||
* Example response:
|
||||
* {
|
||||
* "content_type": "image\/png",
|
||||
* "file_content": "iVBORw.......",
|
||||
* }
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
public function get_file_content( WP_REST_Request $request ) {
|
||||
$file_id = $request->get_param( 'file_id' );
|
||||
$as_account = (bool) $request->get_param( 'as_account' );
|
||||
|
||||
return $this->forward_request( 'get_file_contents', [ $file_id, $as_account ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error response
|
||||
*
|
||||
* @param WP_Error $error - error.
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
private function file_error_response( WP_Error $error ): WP_Error {
|
||||
$error_status_code = 'resource_missing' === $error->get_error_code() ? WP_Http::NOT_FOUND : WP_Http::INTERNAL_SERVER_ERROR;
|
||||
return new WP_Error(
|
||||
$error->get_error_code(),
|
||||
$error->get_error_message(),
|
||||
[ 'status' => $error_status_code ]
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Orders_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for order processing.
|
||||
*/
|
||||
class WC_REST_Payments_Fraud_Outcomes_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/fraud_outcomes';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>\w+)/latest',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_latest_fraud_outcome' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve charge to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_latest_fraud_outcome( $request ) {
|
||||
$id = $request->get_param( 'id' );
|
||||
|
||||
return $this->forward_request( 'get_latest_fraud_outcome', [ $id ] );
|
||||
}
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Onboarding_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Logger;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for account details and status.
|
||||
*/
|
||||
class WC_REST_Payments_Onboarding_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
const RESULT_BAD_REQUEST = 'bad_request';
|
||||
|
||||
/**
|
||||
* Onboarding Service.
|
||||
*
|
||||
* @var WC_Payments_Onboarding_Service
|
||||
*/
|
||||
protected $onboarding_service;
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/onboarding';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
|
||||
* @param WC_Payments_Onboarding_Service $onboarding_service Onboarding Service class instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $api_client,
|
||||
WC_Payments_Onboarding_Service $onboarding_service
|
||||
) {
|
||||
parent::__construct( $api_client );
|
||||
$this->onboarding_service = $onboarding_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/kyc/session',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_embedded_kyc_session' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'progressive' => [
|
||||
'required' => false,
|
||||
'description' => 'Whether the session is for progressive onboarding.',
|
||||
'type' => 'string',
|
||||
],
|
||||
'self_assessment' => [
|
||||
'required' => false,
|
||||
'description' => 'The self-assessment data.',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'country' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The country code where the company is legally registered.',
|
||||
'required' => true,
|
||||
],
|
||||
'business_type' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The company incorporation type.',
|
||||
'required' => true,
|
||||
],
|
||||
'mcc' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The merchant category code. This can either be a true MCC or an MCCs tree item id from the onboarding form.',
|
||||
'required' => true,
|
||||
],
|
||||
'annual_revenue' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The estimated annual revenue bucket id.',
|
||||
'required' => true,
|
||||
],
|
||||
'go_live_timeframe' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The timeframe bucket for the estimated first live transaction.',
|
||||
'required' => true,
|
||||
],
|
||||
'url' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The URL of the store.',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/kyc/finalize',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'finalize_embedded_kyc' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'source' => [
|
||||
'required' => false,
|
||||
'description' => 'The very first entry point the merchant entered our onboarding flow.',
|
||||
'type' => 'string',
|
||||
],
|
||||
'from' => [
|
||||
'required' => false,
|
||||
'description' => 'The previous step in the onboarding flow leading the merchant to arrive at the current step.',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/business_types',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_business_types' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/router/po_eligible',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'args' => [
|
||||
'business' => [
|
||||
'required' => true,
|
||||
'description' => 'The context about the merchant\'s business (self-assessment data).',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'country' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The country code where the company is legally registered.',
|
||||
'required' => true,
|
||||
],
|
||||
'type' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The company incorporation type.',
|
||||
'required' => true,
|
||||
],
|
||||
'mcc' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The merchant category code. This can either be a true MCC or an MCCs tree item id from the onboarding form.',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'store' => [
|
||||
'required' => true,
|
||||
'description' => 'The context about the merchant\'s store (self-assessment data).',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'annual_revenue' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The estimated annual revenue bucket id.',
|
||||
'required' => true,
|
||||
],
|
||||
'go_live_timeframe' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The timeframe bucket for the estimated first live transaction.',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'woo_store_stats' => [
|
||||
'required' => false,
|
||||
'description' => 'Context about the merchant\'s current WooCommerce store.',
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'get_progressive_onboarding_eligible' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an account embedded KYC session via the API.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function get_embedded_kyc_session( WP_REST_Request $request ) {
|
||||
$self_assessment_data = ! empty( $request->get_param( 'self_assessment' ) ) ? wc_clean( wp_unslash( $request->get_param( 'self_assessment' ) ) ) : [];
|
||||
$progressive = ! empty( $request->get_param( 'progressive' ) ) && 'true' === $request->get_param( 'progressive' );
|
||||
|
||||
$account_session = $this->onboarding_service->create_embedded_kyc_session(
|
||||
$self_assessment_data,
|
||||
$progressive
|
||||
);
|
||||
|
||||
if ( $account_session ) {
|
||||
$account_session['locale'] = get_user_locale();
|
||||
}
|
||||
|
||||
return rest_ensure_response( $account_session );
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize the embedded KYC session via the API.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function finalize_embedded_kyc( WP_REST_Request $request ) {
|
||||
$source = $request->get_param( 'source' ) ?? '';
|
||||
$from = $request->get_param( 'from' ) ?? '';
|
||||
$actioned_notes = WC_Payments_Onboarding_Service::get_actioned_notes();
|
||||
|
||||
// Call the API to finalize the onboarding.
|
||||
try {
|
||||
$response = $this->onboarding_service->finalize_embedded_kyc(
|
||||
get_user_locale(),
|
||||
$source,
|
||||
$actioned_notes
|
||||
);
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] );
|
||||
}
|
||||
|
||||
// Handle some post-onboarding tasks and get the redirect params.
|
||||
$finalize = WC_Payments::get_account_service()->finalize_embedded_connection(
|
||||
$response['mode'],
|
||||
[
|
||||
'promo' => $response['promotion_id'] ?? '',
|
||||
'from' => $from,
|
||||
'source' => $source,
|
||||
]
|
||||
);
|
||||
|
||||
// Return the response, the client will handle the redirect.
|
||||
return rest_ensure_response(
|
||||
array_merge(
|
||||
$response,
|
||||
$finalize
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get business types via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_business_types( WP_REST_Request $request ) {
|
||||
$business_types = $this->onboarding_service->get_cached_business_types();
|
||||
return rest_ensure_response( [ 'data' => $business_types ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progressive onboarding eligibility via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_progressive_onboarding_eligible( WP_REST_Request $request ) {
|
||||
return $this->forward_request(
|
||||
'get_onboarding_po_eligible',
|
||||
[
|
||||
'business_info' => $request->get_param( 'business' ),
|
||||
'store_info' => $request->get_param( 'store' ),
|
||||
'woo_store_stats' => $request->get_param( 'woo_store_stats' ) ?? [],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Orders_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use WCPay\Core\Server\Request\Create_Intention;
|
||||
use WCPay\Core\Server\Request\Get_Intention;
|
||||
use WCPay\Logger;
|
||||
use WCPay\Constants\Order_Status;
|
||||
use WCPay\Constants\Intent_Status;
|
||||
use WCPay\Constants\Payment_Method;
|
||||
|
||||
/**
|
||||
* REST controller for order processing.
|
||||
*/
|
||||
class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/orders';
|
||||
|
||||
/**
|
||||
* Instance of WC_Payment_Gateway_WCPay
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* WC_Payments_Customer_Service instance for working with customer information
|
||||
*
|
||||
* @var WC_Payments_Customer_Service
|
||||
*/
|
||||
private $customer_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Order_Service instance for updating order statuses.
|
||||
*
|
||||
* @var WC_Payments_Order_Service
|
||||
*/
|
||||
private $order_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_REST_Controller constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
|
||||
* @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway.
|
||||
* @param WC_Payments_Customer_Service $customer_service Customer class instance.
|
||||
* @param WC_Payments_Order_Service $order_service Order Service class instance.
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service ) {
|
||||
parent::__construct( $api_client );
|
||||
$this->gateway = $gateway;
|
||||
$this->customer_service = $customer_service;
|
||||
$this->order_service = $order_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
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,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/(?P<order_id>\w+)/capture_authorization',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'capture_authorization' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'payment_intent_id' => [
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/(?P<order_id>\w+)/cancel_authorization',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'cancel_authorization' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'payment_intent_id' => [
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/(?P<order_id>\w+)/create_terminal_intent',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'create_terminal_intent' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
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' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an intent ID and an order ID, add the intent ID to the order and capture it.
|
||||
* Use-cases: Mobile apps using it for `card_present` and `interac_present` payment types.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function capture_terminal_payment( WP_REST_Request $request ) {
|
||||
try {
|
||||
$intent_id = $request['payment_intent_id'];
|
||||
$order_id = $request['order_id'];
|
||||
|
||||
// Do not process non-existing orders.
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( false === $order ) {
|
||||
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
// Do not process orders with refund(s).
|
||||
if ( 0 < $order->get_total_refunded() ) {
|
||||
return new WP_Error(
|
||||
'wcpay_refunded_order_uncapturable',
|
||||
__( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
// Do not process already processed orders to prevent double-charging.
|
||||
$processed_order_intent_statuses = [
|
||||
Intent_Status::SUCCEEDED,
|
||||
Intent_Status::CANCELED,
|
||||
Intent_Status::PROCESSING,
|
||||
];
|
||||
$stored_intent_id = $order->get_meta( WC_Payments_Order_Service::INTENT_ID_META_KEY );
|
||||
$stored_intent_status = $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY );
|
||||
if (
|
||||
in_array( $stored_intent_status, $processed_order_intent_statuses, true ) ||
|
||||
( $stored_intent_id && $stored_intent_id !== $intent_id )
|
||||
) {
|
||||
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured for completed or processed orders.', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
|
||||
// Do not process intents that can't be captured.
|
||||
$request = Get_Intention::create( $intent_id );
|
||||
$request->set_hook_args( $order );
|
||||
$intent = $request->send();
|
||||
|
||||
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
|
||||
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
|
||||
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
||||
if ( $intent_meta_order_id !== $order->get_id() ) {
|
||||
Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' );
|
||||
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
if ( ! $intent->is_authorized() ) {
|
||||
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
|
||||
// Update the order: set the payment method and attach intent attributes.
|
||||
$order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
|
||||
$order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) );
|
||||
$this->order_service->attach_intent_info_to_order( $order, $intent );
|
||||
$this->order_service->update_order_status_from_intent( $order, $intent );
|
||||
|
||||
// Certain payments (eg. Interac) are captured on the client-side (mobile app).
|
||||
// The client may send us the captured intent to link it to its WC order.
|
||||
// Doing so via this endpoint is more reliable than depending on the payment_intent.succeeded event.
|
||||
$is_intent_captured = Intent_Status::SUCCEEDED === $intent->get_status();
|
||||
$result_for_captured_intent = [
|
||||
'status' => Intent_Status::SUCCEEDED,
|
||||
'id' => $intent->get_id(),
|
||||
];
|
||||
|
||||
$result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false, $intent_metadata );
|
||||
|
||||
if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
|
||||
$http_code = $result['http_code'] ?? 502;
|
||||
$error_code = $result['error_code'] ?? null;
|
||||
$extra_details = $result['extra_details'] ?? [];
|
||||
return new WP_Error(
|
||||
'wcpay_capture_error',
|
||||
sprintf(
|
||||
// translators: %s: the error message.
|
||||
__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ),
|
||||
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
|
||||
),
|
||||
[
|
||||
'status' => $http_code,
|
||||
'extra_details' => $extra_details,
|
||||
'error_type' => $error_code,
|
||||
]
|
||||
);
|
||||
}
|
||||
// Store receipt generation URL for mobile applications in order meta-data.
|
||||
$order->add_meta_data( 'receipt_url', get_rest_url( null, sprintf( '%s/payments/readers/receipts/%s', $this->namespace, $intent->get_id() ) ) );
|
||||
|
||||
// Add payment method for future subscription payments.
|
||||
$generated_card = $intent->get_charge()->get_payment_method_details()[ Payment_Method::CARD_PRESENT ]['generated_card'] ?? null;
|
||||
// If we don't get a generated card, e.g. because a digital wallet was used, we can still return that the initial payment was successful.
|
||||
// The subscription will not be activated and customers will need to provide a new payment method for renewals.
|
||||
if ( $generated_card ) {
|
||||
$has_subscriptions = function_exists( 'wcs_order_contains_subscription' ) &&
|
||||
function_exists( 'wcs_get_subscriptions_for_order' ) &&
|
||||
function_exists( 'wcs_is_manual_renewal_required' ) &&
|
||||
wcs_order_contains_subscription( $order_id );
|
||||
if ( $has_subscriptions ) {
|
||||
$token = WC_Payments::get_token_service()->add_payment_method_to_user( $generated_card, $order->get_user() );
|
||||
$this->gateway->add_token_to_order( $order, $token );
|
||||
foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription ) {
|
||||
$subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
|
||||
// Where the setting doesn't force manual renewals, we should turn them off, because we have an auto-renewal token now.
|
||||
if ( ! wcs_is_manual_renewal_required() ) {
|
||||
$subscription->set_requires_manual_renewal( false );
|
||||
}
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualize order status.
|
||||
$this->order_service->mark_terminal_payment_completed( $order, $intent_id, $result['status'] );
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'status' => $result['status'],
|
||||
'id' => $result['id'],
|
||||
]
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to capture a terminal payment via REST API: ' . $e );
|
||||
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures an authorization.
|
||||
* Use-cases: Merchants manually capturing a payment when they enable "capture later" option.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function capture_authorization( WP_REST_Request $request ) {
|
||||
try {
|
||||
$intent_id = $request['payment_intent_id'];
|
||||
$order_id = $request['order_id'];
|
||||
|
||||
// Do not process non-existing orders.
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( false === $order ) {
|
||||
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
// Do not process orders with refund(s).
|
||||
if ( 0 < $order->get_total_refunded() ) {
|
||||
return new WP_Error(
|
||||
'wcpay_refunded_order_uncapturable',
|
||||
__( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
// Do not process intents that can't be captured.
|
||||
$request = Get_Intention::create( $intent_id );
|
||||
$request->set_hook_args( $order );
|
||||
$intent = $request->send();
|
||||
|
||||
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
|
||||
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
|
||||
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
||||
if ( $intent_meta_order_id !== $order->get_id() ) {
|
||||
Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' );
|
||||
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
if ( ! $intent->is_authorized() ) {
|
||||
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
|
||||
$this->add_fraud_outcome_manual_entry( $order, 'approve' );
|
||||
|
||||
$result = $this->gateway->capture_charge( $order, true, $intent_metadata );
|
||||
|
||||
if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
|
||||
$error_code = $result['error_code'] ?? null;
|
||||
$extra_details = $result['extra_details'] ?? [];
|
||||
return new WP_Error(
|
||||
'wcpay_capture_error',
|
||||
sprintf(
|
||||
// translators: %s: the error message.
|
||||
__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ),
|
||||
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
|
||||
),
|
||||
[
|
||||
'status' => $result['http_code'] ?? 502,
|
||||
'extra_details' => $extra_details,
|
||||
'error_type' => $error_code,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$order->save_meta_data();
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'status' => $result['status'],
|
||||
'id' => $result['id'],
|
||||
]
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to capture an authorization via REST API: ' . $e );
|
||||
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns customer id from order. Create or update customer if needed.
|
||||
* Use-cases:
|
||||
* - It was used by older versions of our mobile apps to add the customer details to Payment Intents.
|
||||
* - It is used by the apps to set customer details on Payment Intents for an order containing subscriptions. Required for capturing renewal payments off session.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function create_customer( $request ) {
|
||||
try {
|
||||
$order_id = $request['order_id'];
|
||||
|
||||
// Do not process non-existing orders.
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( false === $order || ! ( $order instanceof WC_Order ) ) {
|
||||
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
$disallowed_order_statuses = apply_filters(
|
||||
'wcpay_create_customer_disallowed_order_statuses',
|
||||
[
|
||||
Order_Status::COMPLETED,
|
||||
Order_Status::CANCELLED,
|
||||
Order_Status::REFUNDED,
|
||||
Order_Status::FAILED,
|
||||
]
|
||||
);
|
||||
if ( $order->has_status( $disallowed_order_statuses ) ) {
|
||||
return new WP_Error( 'wcpay_invalid_order_status', __( 'Invalid order status', 'woocommerce-payments' ), [ 'status' => 400 ] );
|
||||
}
|
||||
|
||||
$order_user = $order->get_user();
|
||||
$customer_id = $this->order_service->get_customer_id_for_order( $order );
|
||||
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
|
||||
$is_guest_customer = false === $order_user;
|
||||
|
||||
// If the order is created for a registered customer, try extracting it's Stripe customer ID.
|
||||
if ( ! $customer_id && ! $is_guest_customer ) {
|
||||
$customer_id = $this->customer_service->get_customer_id_by_user_id( $order_user->ID );
|
||||
}
|
||||
|
||||
$order_user = $is_guest_customer ? new WP_User() : $order_user;
|
||||
$customer_id = $customer_id
|
||||
? $this->customer_service->update_customer_for_user( $customer_id, $order_user, $customer_data )
|
||||
: $this->customer_service->create_customer_for_user( $order_user, $customer_data );
|
||||
|
||||
$this->order_service->set_customer_id_for_order( $order, $customer_id );
|
||||
$order->save();
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'id' => $customer_id,
|
||||
]
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to create / update customer from order via REST API: ' . $e );
|
||||
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new in-person payment intent for the given order ID without confirming it.
|
||||
* Use-cases: Mobile apps using it for `card_present` payment types. (`interac_present` is handled by the apps via Stripe SDK).
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function create_terminal_intent( $request ) {
|
||||
// Do not process non-existing orders.
|
||||
$order = wc_get_order( $request['order_id'] );
|
||||
if ( false === $order ) {
|
||||
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
||||
}
|
||||
try {
|
||||
$currency = strtolower( $order->get_currency() );
|
||||
$customer_id = $request->get_param( 'customer_id' );
|
||||
$metadata = $request->get_param( 'metadata' ) ?? [];
|
||||
$metadata['order_number'] = $order->get_order_number();
|
||||
|
||||
$wcpay_server_request = Create_Intention::create();
|
||||
$wcpay_server_request->set_currency_code( $currency );
|
||||
$wcpay_server_request->set_amount( WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ) );
|
||||
if ( $customer_id ) {
|
||||
$wcpay_server_request->set_customer( $customer_id );
|
||||
}
|
||||
$wcpay_server_request->set_metadata( $metadata );
|
||||
$wcpay_server_request->set_payment_method_types( $this->get_terminal_intent_payment_method( $request ) );
|
||||
$wcpay_server_request->set_capture_method( 'manual' === $this->get_terminal_intent_capture_method( $request ) );
|
||||
$wcpay_server_request->set_hook_args( $order );
|
||||
$intent = $wcpay_server_request->send();
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'id' => ! empty( $intent ) ? $intent->get_id() : null,
|
||||
]
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to create an intention via REST API: ' . $e );
|
||||
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return terminal intent payment method array based on payment methods request.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @param array $default_value - default value.
|
||||
*
|
||||
* @return array|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ): array {
|
||||
$payment_methods = $request->get_param( 'payment_methods' );
|
||||
if ( null === $payment_methods ) {
|
||||
return $default_value;
|
||||
}
|
||||
|
||||
if ( ! is_array( $payment_methods ) ) {
|
||||
throw new \Exception( 'Invalid param \'payment_methods\'!' );
|
||||
}
|
||||
|
||||
foreach ( $payment_methods as $value ) {
|
||||
if ( ! in_array( $value, Payment_Method::IPP_ALLOWED_PAYMENT_METHODS, true ) ) {
|
||||
throw new \Exception( 'One or more payment methods are not supported!' );
|
||||
}
|
||||
}
|
||||
|
||||
return $payment_methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return terminal intent capture method based on capture method request.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @param string $default_value default value.
|
||||
*
|
||||
* @return string|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ): string {
|
||||
$capture_method = $request->get_param( 'capture_method' );
|
||||
if ( null === $capture_method ) {
|
||||
return $default_value;
|
||||
}
|
||||
|
||||
if ( ! in_array( $capture_method, [ 'manual', 'automatic' ], true ) ) {
|
||||
throw new \Exception( 'Invalid param \'capture_method\'!' );
|
||||
}
|
||||
|
||||
return $capture_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an authorization.
|
||||
* Use-cases: Merchants manually canceling when blocking an on hold review by Fraud & Risk tools.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function cancel_authorization( WP_REST_Request $request ) {
|
||||
try {
|
||||
$intent_id = $request['payment_intent_id'];
|
||||
$order_id = $request['order_id'];
|
||||
|
||||
// Do not process non-existing orders.
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( false === $order ) {
|
||||
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
// Do not process orders with refund(s).
|
||||
if ( 0 < $order->get_total_refunded() ) {
|
||||
return new WP_Error(
|
||||
'wcpay_refunded_order_uncapturable',
|
||||
__( 'Payment cannot be canceled for partially or fully refunded orders.', 'woocommerce-payments' ),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
// Do not process intents that can't be canceled.
|
||||
$request = Get_Intention::create( $intent_id );
|
||||
$request->set_hook_args( $order );
|
||||
$intent = $request->send();
|
||||
|
||||
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
|
||||
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
|
||||
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
||||
if ( $intent_meta_order_id !== $order->get_id() ) {
|
||||
Logger::error( 'Payment cancellation rejected due to failed validation: order id on intent is incorrect or missing.' );
|
||||
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be canceled', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
if ( ! in_array( $intent->get_status(), [ Intent_Status::REQUIRES_CAPTURE ], true ) ) {
|
||||
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be canceled', 'woocommerce-payments' ), [ 'status' => 409 ] );
|
||||
}
|
||||
|
||||
$this->add_fraud_outcome_manual_entry( $order, 'block' );
|
||||
|
||||
$result = $this->gateway->cancel_authorization( $order );
|
||||
|
||||
if ( Intent_Status::CANCELED !== $result['status'] ) {
|
||||
return new WP_Error(
|
||||
'wcpay_cancel_error',
|
||||
sprintf(
|
||||
// translators: %s: the error message.
|
||||
__( 'Payment cancel failed to complete with the following message: %s', 'woocommerce-payments' ),
|
||||
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
|
||||
),
|
||||
[ 'status' => $result['http_code'] ?? 502 ]
|
||||
);
|
||||
}
|
||||
|
||||
$order->save_meta_data();
|
||||
|
||||
return rest_ensure_response(
|
||||
[
|
||||
'status' => $result['status'],
|
||||
'id' => $result['id'],
|
||||
]
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to cancel an authorization via REST API: ' . $e );
|
||||
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the fraud_outcome_manual_entry meta to the order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string $action User action.
|
||||
*/
|
||||
private function add_fraud_outcome_manual_entry( $order, $action ) {
|
||||
$current_user = wp_get_current_user();
|
||||
$order->add_meta_data(
|
||||
'_wcpay_fraud_outcome_manual_entry',
|
||||
[
|
||||
'type' => 'fraud_outcome_manual_' . $action,
|
||||
'user' => [
|
||||
'id' => $current_user->ID,
|
||||
'username' => $current_user->user_login,
|
||||
],
|
||||
'action' => 'block' === $action ? 'blocked' : 'approved',
|
||||
'datetime' => time(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Payment_Intents_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
|
||||
use WCPay\Logger;
|
||||
use WCPay\Exceptions\Rest_Request_Exception;
|
||||
use WCPay\Constants\Payment_Type;
|
||||
use WCPay\Internal\Service\Level3Service;
|
||||
use WCPay\Internal\Service\OrderService;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for charges.
|
||||
*/
|
||||
class WC_REST_Payments_Payment_Intents_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/payment_intents';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<payment_intent_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_payment_intent' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve charge to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_payment_intent( $request ) {
|
||||
$payment_intent_id = $request->get_param( 'payment_intent_id' );
|
||||
|
||||
return $this->forward_request( 'get_intent', [ $payment_intent_id ] );
|
||||
}
|
||||
}
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Payment_Intents_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
|
||||
use WCPay\Logger;
|
||||
use WCPay\Exceptions\Rest_Request_Exception;
|
||||
use WCPay\Constants\Payment_Type;
|
||||
use WCPay\Internal\Service\Level3Service;
|
||||
use WCPay\Internal\Service\OrderService;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for charges.
|
||||
*/
|
||||
class WC_REST_Payments_Payment_Intents_Create_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Instance of WC_Payment_Gateway_WCPay
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* Order service instance.
|
||||
*
|
||||
* @var OrderService
|
||||
*/
|
||||
private $order_service;
|
||||
|
||||
/**
|
||||
* Level3 service instance.
|
||||
*
|
||||
* @var Level3Service
|
||||
*/
|
||||
private $level3_service;
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/payment_intents';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'create_payment_intent' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'schema' => [ $this, 'get_item_schema' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* WC_REST_Payments_Payment_Intents_Create_Controller constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
|
||||
* @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway.
|
||||
* @param OrderService $order_service The new order servie.
|
||||
* @param Level3Service $level3_service Level3 service instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $api_client,
|
||||
WC_Payment_Gateway_WCPay $gateway,
|
||||
OrderService $order_service,
|
||||
Level3Service $level3_service
|
||||
) {
|
||||
parent::__construct( $api_client );
|
||||
|
||||
$this->gateway = $gateway;
|
||||
$this->order_service = $order_service;
|
||||
$this->level3_service = $level3_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment intent.
|
||||
*
|
||||
* @param WP_REST_Request $request data about the request.
|
||||
*
|
||||
* @throws Rest_Request_Exception
|
||||
*/
|
||||
public function create_payment_intent( $request ) {
|
||||
try {
|
||||
|
||||
$order_id = $request->get_param( 'order_id' );
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
throw new Rest_Request_Exception( __( 'Order not found', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
$wcpay_server_request = Create_And_Confirm_Intention::create();
|
||||
|
||||
$currency = strtolower( $order->get_currency() );
|
||||
$amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency );
|
||||
$wcpay_server_request->set_currency_code( $currency );
|
||||
$wcpay_server_request->set_amount( $amount );
|
||||
|
||||
$metadata = $this->order_service->get_payment_metadata( $order_id, Payment_Type::SINGLE() );
|
||||
$wcpay_server_request->set_metadata( $metadata );
|
||||
|
||||
$wcpay_server_request->set_customer( $request->get_param( 'customer' ) );
|
||||
$wcpay_server_request->set_level3( $this->level3_service->get_data_from_order( $order_id ) );
|
||||
$wcpay_server_request->set_payment_method( $request->get_param( 'payment_method' ) );
|
||||
$wcpay_server_request->set_payment_method_types( [ 'card' ] );
|
||||
$wcpay_server_request->set_off_session( true );
|
||||
$wcpay_server_request->set_capture_method( $this->gateway->get_option( 'manual_capture' ) && ( 'yes' === $this->gateway->get_option( 'manual_capture' ) ) );
|
||||
|
||||
$wcpay_server_request->assign_hook( 'wcpay_create_and_confirm_intent_request_api' );
|
||||
$intent = $wcpay_server_request->send();
|
||||
|
||||
$response = $this->prepare_item_for_response( $intent, $request );
|
||||
return rest_ensure_response( $this->prepare_response_for_collection( $response ) );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to create an intention via REST API: ' . $e );
|
||||
return new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Item schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return [
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'payment_intent',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => [
|
||||
'description' => __( 'ID for the payment intent.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'amount' => [
|
||||
'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ),
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'currency' => [
|
||||
'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'created' => [
|
||||
'description' => __( 'The date when the payment intent was created.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'customer' => [
|
||||
'description' => __( 'The customer id of the intent', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'status' => [
|
||||
'description' => __( 'The status of the payment intent.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'charge' => [
|
||||
'description' => __( 'Charge object associated with this payment intention.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'id' => [
|
||||
'description' => 'ID for the charge.',
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'amount' => [
|
||||
'description' => 'The amount of the charge.',
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'payment_method_details' => [
|
||||
'description' => 'Details for the payment method used for the charge.',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'card' => [
|
||||
'description' => 'Details for a card payment method.',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'amount_authorized' => [
|
||||
'description' => 'The amount authorized by the card.',
|
||||
'type' => 'integer',
|
||||
],
|
||||
'brand' => [
|
||||
'description' => 'The brand of the card.',
|
||||
'type' => 'string',
|
||||
],
|
||||
'capture_before' => [
|
||||
'description' => 'Timestamp for when the authorization must be captured.',
|
||||
'type' => 'string',
|
||||
],
|
||||
'country' => [
|
||||
'description' => 'The ISO country code.',
|
||||
'type' => 'string',
|
||||
],
|
||||
'exp_month' => [
|
||||
'description' => 'The expiration month of the card.',
|
||||
'type' => 'integer',
|
||||
],
|
||||
'exp_year' => [
|
||||
'description' => 'The expiration year of the card.',
|
||||
'type' => 'integer',
|
||||
],
|
||||
'last4' => [
|
||||
'description' => 'The last 4 digits of the card.',
|
||||
'type' => 'string',
|
||||
],
|
||||
'three_d_secure' => [
|
||||
'description' => 'Details for 3D Secure authentication.',
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'billing_details' => [
|
||||
'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'address' => [
|
||||
'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view' ],
|
||||
'properties' => [
|
||||
'city' => [
|
||||
'description' => __( 'City of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'country' => [
|
||||
'description' => __( 'Country of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'line1' => [
|
||||
'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'line2' => [
|
||||
'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'postal_code' => [
|
||||
'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'state' => [
|
||||
'description' => __( 'State of the billing address.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
'email' => [
|
||||
'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'format' => 'email',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'name' => [
|
||||
'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'phone' => [
|
||||
'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
'payment_method' => [
|
||||
'description' => 'The payment method associated with this charge.',
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'application_fee_amount' => [
|
||||
'description' => 'The application fee amount.',
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
'status' => [
|
||||
'description' => 'The status of the payment intent created.',
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare each item for response.
|
||||
*
|
||||
* @param array|mixed $item Item to prepare.
|
||||
* @param WP_REST_Request $request Request instance.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function prepare_item_for_response( $item, $request ) {
|
||||
$prepared_item = [];
|
||||
$prepared_item['id'] = $item->get_id();
|
||||
$prepared_item['amount'] = $item->get_amount();
|
||||
$prepared_item['currency'] = $item->get_currency();
|
||||
$prepared_item['created'] = $item->get_created()->format( 'Y-m-d H:i:s' );
|
||||
$prepared_item['customer'] = $item->get_customer_id();
|
||||
$prepared_item['payment_method'] = $item->get_payment_method_id();
|
||||
$prepared_item['status'] = $item->get_status();
|
||||
|
||||
try {
|
||||
$charge = $item->get_charge();
|
||||
$prepared_item['charge']['id'] = $charge->get_id();
|
||||
$prepared_item['charge']['amount'] = $charge->get_amount();
|
||||
$prepared_item['charge']['application_fee_amount'] = $charge->get_application_fee_amount();
|
||||
$prepared_item['charge']['status'] = $charge->get_status();
|
||||
|
||||
$billing_details = $charge->get_billing_details();
|
||||
if ( isset( $billing_details['address'] ) ) {
|
||||
$prepared_item['charge']['billing_details']['address']['city'] = $billing_details['address']['city'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['address']['country'] = $billing_details['address']['country'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['address']['line1'] = $billing_details['address']['line1'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['address']['line2'] = $billing_details['address']['line2'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['address']['postal_code'] = $billing_details['address']['postal_code'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['address']['state'] = $billing_details['address']['state'] ?? '';
|
||||
}
|
||||
$prepared_item['charge']['billing_details']['email'] = $billing_details['email'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['name'] = $billing_details['name'] ?? '';
|
||||
$prepared_item['charge']['billing_details']['phone'] = $billing_details['phone'] ?? '';
|
||||
|
||||
$payment_method_details = $charge->get_payment_method_details();
|
||||
if ( isset( $payment_method_details['card'] ) ) {
|
||||
$prepared_item['charge']['payment_method_details']['card']['amount_authorized'] = $payment_method_details['card']['amount_authorized'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['brand'] = $payment_method_details['card']['brand'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['capture_before'] = $payment_method_details['card']['capture_before'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['country'] = $payment_method_details['card']['country'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['exp_month'] = $payment_method_details['card']['exp_month'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['exp_year'] = $payment_method_details['card']['exp_year'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['last4'] = $payment_method_details['card']['last4'] ?? '';
|
||||
$prepared_item['charge']['payment_method_details']['card']['three_d_secure'] = $payment_method_details['card']['three_d_secure'] ?? '';
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
Logger::error( 'Failed to prepare payment intent for response: ' . $e );
|
||||
}
|
||||
|
||||
$context = $request['context'] ?? 'view';
|
||||
$prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request );
|
||||
$prepared_item = $this->filter_response_by_context( $prepared_item, $context );
|
||||
|
||||
return rest_ensure_response( $prepared_item );
|
||||
}
|
||||
}
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Reader_Charges
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Get_Charge;
|
||||
use WCPay\Core\Server\Request\Get_Intention;
|
||||
use WCPay\Constants\Intent_Status;
|
||||
use WCPay\Core\Server\Request;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
require_once WCPAY_ABSPATH . 'includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php';
|
||||
|
||||
/**
|
||||
* REST controller for reader charges.
|
||||
*/
|
||||
class WC_REST_Payments_Reader_Controller extends WC_Payments_REST_Controller {
|
||||
const STORE_READERS_TRANSIENT_KEY = 'wcpay_store_terminal_readers';
|
||||
|
||||
const PREVIEW_RECEIPT_CHARGE_DATA = [
|
||||
'amount_captured' => 0,
|
||||
'payment_method_details' => [
|
||||
'card_present' => [
|
||||
'brand' => 'Sample',
|
||||
'last4' => '0000',
|
||||
'receipt' => [
|
||||
'application_preferred_name' => 'Sample, Receipts preview',
|
||||
'dedicated_file_name' => '0000',
|
||||
'account_type' => 'Sample',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/readers';
|
||||
|
||||
/**
|
||||
* Instance of WC_Payment_Gateway_WCPay.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $wcpay_gateway;
|
||||
|
||||
/**
|
||||
* Instance of WC_Payments_In_Person_Payments_Receipts_Service.
|
||||
*
|
||||
* @var WC_Payments_In_Person_Payments_Receipts_Service
|
||||
*/
|
||||
private $receipts_service;
|
||||
|
||||
/**
|
||||
* WC_REST_Payments_Reader_Controller
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WC_Payments_API_Client.
|
||||
* @param WC_Payment_Gateway_WCPay $wcpay_gateway WC_Payment_Gateway_WCPay.
|
||||
* @param WC_Payments_In_Person_Payments_Receipts_Service $receipts_service WC_Payments_In_Person_Payments_Receipts_Service.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $wcpay_gateway, WC_Payments_In_Person_Payments_Receipts_Service $receipts_service ) {
|
||||
parent::__construct( $api_client );
|
||||
|
||||
$this->wcpay_gateway = $wcpay_gateway;
|
||||
$this->receipts_service = $receipts_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_all_readers' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'register_reader' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'location' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
'registration_code' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
'label' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
'metadata' => [
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/charges/(?P<transaction_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/receipts/preview',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'preview_print_receipt' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/receipts/(?P<payment_intent_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'generate_print_receipt' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment readers charges to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_summary( $request ) {
|
||||
|
||||
$transaction_id = $request->get_param( 'transaction_id' );
|
||||
|
||||
try {
|
||||
// retrieve transaction details to get the charge date.
|
||||
$transaction = $this->api_client->get_transaction( $transaction_id );
|
||||
|
||||
if ( empty( $transaction ) ) {
|
||||
return rest_ensure_response( [] );
|
||||
}
|
||||
$summary = $this->api_client->get_readers_charge_summary( gmdate( 'Y-m-d', $transaction['created'] ) );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( 'wcpay_get_summary', $e->getMessage() ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $summary );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies the get all readers request to the server.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_all_readers( $request ) {
|
||||
try {
|
||||
return rest_ensure_response( $this->fetch_readers() );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a card reader to an account and terminal location.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function register_reader( $request ) {
|
||||
try {
|
||||
$response = $this->api_client->register_terminal_reader(
|
||||
$request->get_param( 'location' ),
|
||||
$request->get_param( 'registration_code' ),
|
||||
$request->get_param( 'label' ),
|
||||
$request->get_param( 'metadata' )
|
||||
);
|
||||
|
||||
$reader = wp_array_slice_assoc( $response, [ 'id', 'livemode', 'device_type', 'label', 'location', 'metadata', 'status' ] );
|
||||
|
||||
return rest_ensure_response( $reader );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response(
|
||||
new WP_Error(
|
||||
$e->get_error_code(),
|
||||
$e->getMessage(),
|
||||
[ 'status' => $e->get_http_code() ]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the reader status is active
|
||||
*
|
||||
* @param array $readers The readers charges object.
|
||||
* @param string $id The reader ID.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_reader_active( $readers, $id ) {
|
||||
foreach ( $readers as $reader ) {
|
||||
if ( $reader['reader_id'] === $id && 'active' === $reader['status'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read readers from transient and re-populates it if needed.
|
||||
*
|
||||
* @return array Terminal readers.
|
||||
* @throws API_Exception If request to server fails.
|
||||
*/
|
||||
private function fetch_readers(): array {
|
||||
$readers = get_transient( static::STORE_READERS_TRANSIENT_KEY );
|
||||
|
||||
if ( ! $readers ) {
|
||||
// Retrieve terminal readers.
|
||||
$request = Request::get( WC_Payments_API_Client::TERMINAL_READERS_API );
|
||||
$request->assign_hook( 'wcpay_get_terminal_readers_request' );
|
||||
|
||||
$readers_data = $request->send();
|
||||
|
||||
// Retrieve the readers by charges.
|
||||
$reader_by_charges = $this->api_client->get_readers_charge_summary( gmdate( 'Y-m-d', time() ) );
|
||||
|
||||
$readers = [];
|
||||
foreach ( $readers_data as $reader ) {
|
||||
$readers[] = [
|
||||
'id' => $reader['id'],
|
||||
'livemode' => $reader['livemode'],
|
||||
'device_type' => $reader['device_type'],
|
||||
'label' => $reader['label'],
|
||||
'location' => $reader['location'],
|
||||
'metadata' => $reader['metadata'],
|
||||
'status' => $reader['status'],
|
||||
'is_active' => $this->is_reader_active( $reader_by_charges, $reader['id'] ),
|
||||
];
|
||||
}
|
||||
|
||||
set_transient( static::STORE_READERS_TRANSIENT_KEY, $readers, 2 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $readers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders HTML for a print receipt
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_HTTP_Response|WP_Error
|
||||
* @throws \RuntimeException Error collecting data.
|
||||
*/
|
||||
public function generate_print_receipt( $request ) {
|
||||
try {
|
||||
/* Collect the data, available on the server side. */
|
||||
$wcpay_request = Get_Intention::create( $request->get_param( 'payment_intent_id' ) );
|
||||
$payment_intent = $wcpay_request->send();
|
||||
if ( Intent_Status::SUCCEEDED !== $payment_intent->get_status() ) {
|
||||
throw new \RuntimeException( __( 'Invalid payment intent', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
$charge = $payment_intent->get_charge();
|
||||
$charge_id = $charge ? $charge->get_id() : null;
|
||||
$charge_request = Get_Charge::create( $charge_id );
|
||||
$charge_array = $charge_request->send();
|
||||
|
||||
/* Collect receipt data, stored on the store side. */
|
||||
$order = wc_get_order( $charge_array['order']['number'] );
|
||||
if ( false === $order ) {
|
||||
throw new \RuntimeException( __( 'Order not found', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
// Retrieve branding logo file ID.
|
||||
$branding_logo = $this->wcpay_gateway->get_option( 'account_branding_logo', '' );
|
||||
|
||||
/* Collect merchant settings */
|
||||
$settings = [
|
||||
'branding_logo' => ( ! empty( $branding_logo ) ) ? $this->api_client->get_file_contents( $branding_logo, false ) : [],
|
||||
'business_name' => $this->wcpay_gateway->get_option( 'account_business_name' ),
|
||||
'support_info' => [
|
||||
'address' => $this->wcpay_gateway->get_option( 'account_business_support_address' ),
|
||||
'phone' => $this->wcpay_gateway->get_option( 'account_business_support_phone' ),
|
||||
'email' => $this->wcpay_gateway->get_option( 'account_business_support_email' ),
|
||||
],
|
||||
];
|
||||
|
||||
/* Generate receipt */
|
||||
$response = [ 'html_content' => $this->receipts_service->get_receipt_markup( $settings, $order, $charge_array ) ];
|
||||
} catch ( \Throwable $e ) {
|
||||
$error_status_code = $e instanceof API_Exception ? $e->get_http_code() : 500;
|
||||
$response = new WP_Error( 'generate_print_receipt_error', $e->getMessage(), [ 'status' => $error_status_code ] );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
/**
|
||||
* Returns HTML to preview a print receipt
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_HTTP_Response|WP_Error
|
||||
* @throws \RuntimeException Error collecting data.
|
||||
*/
|
||||
public function preview_print_receipt( WP_REST_Request $request ) {
|
||||
$preview = $this->receipts_service->get_receipt_markup(
|
||||
$this->create_print_preview_receipt_settings_data( $request->get_json_params() ),
|
||||
new WC_Payments_Printed_Receipt_Sample_Order(),
|
||||
self::PREVIEW_RECEIPT_CHARGE_DATA
|
||||
);
|
||||
|
||||
return rest_ensure_response( [ 'html_content' => $preview ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates settings data to be used on the printed receipt preview. Defaults to stored settings if one parameter is not provided.
|
||||
*
|
||||
* @param array $receipt_settings Array of settings to use to create the receipt preview.
|
||||
* @return array
|
||||
*/
|
||||
private function create_print_preview_receipt_settings_data( array $receipt_settings ): array {
|
||||
$support_address = empty( $receipt_settings['accountBusinessSupportAddress'] ) ? $this->wcpay_gateway->get_option( 'account_business_support_address' ) : $receipt_settings['accountBusinessSupportAddress'];
|
||||
return [
|
||||
'business_name' => empty( $receipt_settings['accountBusinessName'] ) ? $this->wcpay_gateway->get_option( 'account_business_name' ) : $receipt_settings['accountBusinessName'],
|
||||
'support_info' => [
|
||||
'address' => [
|
||||
'line1' => $support_address['line1'],
|
||||
'line2' => $support_address['line2'],
|
||||
'city' => $support_address['city'],
|
||||
'state' => $support_address['state'],
|
||||
'postal_code' => $support_address['postal_code'],
|
||||
'country' => $support_address['country'],
|
||||
],
|
||||
'phone' => empty( $receipt_settings['accountBusinessSupportPhone'] ) ? $this->wcpay_gateway->get_option( 'account_business_support_phone' ) : $receipt_settings['accountBusinessSupportPhone'],
|
||||
'email' => empty( $receipt_settings['accountBusinessSupportEmail'] ) ? $this->wcpay_gateway->get_option( 'account_business_support_email' ) : $receipt_settings['accountBusinessSupportEmail'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Timeline_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Refund_Charge;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for the timeline, which includes all events related to an intention.
|
||||
*/
|
||||
class WC_REST_Payments_Refunds_Controller extends WC_Payments_REST_Controller {
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/refund';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'process_refund' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes direct refund bypassing any order checks.
|
||||
*
|
||||
* @internal Not intended for usage in integrations or outside of WooCommerce Payments.
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function process_refund( $request ) {
|
||||
$order_id = $request->get_param( 'order_id' );
|
||||
$charge_id = $request->get_param( 'charge_id' );
|
||||
$amount = $request->get_param( 'amount' );
|
||||
$reason = $request->get_param( 'reason' );
|
||||
|
||||
if ( $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( $order ) {
|
||||
$result = wc_create_refund(
|
||||
[
|
||||
'amount' => WC_Payments_Utils::interpret_stripe_amount( $amount, $order->get_currency() ),
|
||||
'reason' => $reason,
|
||||
'order_id' => $order_id,
|
||||
'refund_payment' => true,
|
||||
'restock_items' => true,
|
||||
]
|
||||
);
|
||||
|
||||
return rest_ensure_response( $result );
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$refund_request = Refund_Charge::create( $charge_id );
|
||||
$refund_request->set_charge( $charge_id );
|
||||
$refund_request->set_amount( $amount );
|
||||
$refund_request->set_reason( $reason );
|
||||
$refund_request->set_source( 'transaction_details_no_order' );
|
||||
$response = $refund_request->send();
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( 'wcpay_refund_payment', $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Reporting_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Get_Reporting_Payment_Activity;
|
||||
use WCPay\Core\Server\Request\Request_Utils;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for customers.
|
||||
*/
|
||||
class WC_REST_Payments_Reporting_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/reporting';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/payment_activity',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_payment_activity' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Payment Activity data.
|
||||
*
|
||||
* @param WP_REST_Request $request The request.
|
||||
*/
|
||||
public function get_payment_activity( $request ) {
|
||||
$wcpay_request = Get_Reporting_Payment_Activity::create();
|
||||
$date_start_in_store_timezone = $this->format_date_to_wp_timezone( $request->get_param( 'date_start' ) );
|
||||
$date_end_in_store_timezone = $this->format_date_to_wp_timezone( $request->get_param( 'date_end' ) );
|
||||
$wcpay_request->set_date_start( $date_start_in_store_timezone );
|
||||
$wcpay_request->set_date_end( $date_end_in_store_timezone );
|
||||
$wcpay_request->set_timezone( $request->get_param( 'timezone' ) );
|
||||
$wcpay_request->set_currency( $request->get_param( 'currency' ) );
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Formats a date string to the WordPress timezone.
|
||||
*
|
||||
* @param string $date_string The date string to be formatted.
|
||||
* @return string The formatted date string in the 'Y-m-d\TH:i:s' format.
|
||||
*/
|
||||
private function format_date_to_wp_timezone( $date_string ) {
|
||||
$date = Request_Utils::format_transaction_date_by_timezone( $date_string, '+00:00' );
|
||||
$date = new DateTime( $date );
|
||||
return $date->format( 'Y-m-d\\TH:i:s' );
|
||||
}
|
||||
}
|
||||
+1098
File diff suppressed because it is too large
Load Diff
+149
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Survey_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for settings.
|
||||
*/
|
||||
class WC_REST_Payments_Survey_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v3';
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/survey';
|
||||
|
||||
/**
|
||||
* The HTTP client, used to forward the request to WPCom.
|
||||
*
|
||||
* @var WC_Payments_Http
|
||||
*/
|
||||
protected $http_client;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
* WC_REST_Payments_Survey_Controller constructor.
|
||||
*
|
||||
* @param WC_Payments_Http_Interface $http_client - The HTTP client, used to forward the request to WPCom.
|
||||
*/
|
||||
public function __construct( WC_Payments_Http_Interface $http_client ) {
|
||||
$this->http_client = $http_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/payments-overview',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'submit_payments_overview_survey' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'rating' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'enum' => [
|
||||
'very-unhappy',
|
||||
'unhappy',
|
||||
'neutral',
|
||||
'happy',
|
||||
'very-happy',
|
||||
],
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
],
|
||||
'comments' => [
|
||||
'type' => 'string',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_filter_nohtml_kses',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the overview survey trhough the WPcom API.
|
||||
*
|
||||
* @param WP_REST_Request $request the request being made.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function submit_payments_overview_survey( WP_REST_Request $request ): WP_REST_Response {
|
||||
$comments = $request->get_param( 'comments' ) ?? '';
|
||||
$rating = $request->get_param( 'rating' ) ?? '';
|
||||
|
||||
if ( empty( $rating ) ) {
|
||||
return new WP_REST_Response(
|
||||
[
|
||||
'success' => false,
|
||||
'err' => 'No answers provided',
|
||||
],
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
$request_args = [
|
||||
'url' => WC_Payments_API_Client::ENDPOINT_BASE . '/marketing/survey',
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Forwarded-For' => \WC_Geolocation::get_ip_address(),
|
||||
],
|
||||
];
|
||||
$request_body = wp_json_encode(
|
||||
[
|
||||
'site_id' => $this->http_client->get_blog_id(),
|
||||
'survey_id' => 'wcpay-payment-activity',
|
||||
'survey_responses' => [
|
||||
'rating' => $rating,
|
||||
'comments' => [ 'text' => $comments ],
|
||||
'wcpay-version' => [ 'text' => WCPAY_VERSION_NUMBER ],
|
||||
],
|
||||
]
|
||||
);
|
||||
$is_site_specific = true;
|
||||
$use_user_token = true;
|
||||
|
||||
$wpcom_response = $this->http_client->remote_request(
|
||||
$request_args,
|
||||
$request_body,
|
||||
$is_site_specific,
|
||||
$use_user_token
|
||||
);
|
||||
|
||||
$wpcom_response_status_code = wp_remote_retrieve_response_code( $wpcom_response );
|
||||
|
||||
if ( 200 === $wpcom_response_status_code ) {
|
||||
update_option( 'wcpay_survey_payment_overview_submitted', true );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $wpcom_response, $wpcom_response_status_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify access.
|
||||
*
|
||||
* Override this method if custom permissions required.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_permission() {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
}
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Terminal_Locations_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use WCPay\Core\Server\Request;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
/**
|
||||
* REST controller for account details and status.
|
||||
*/
|
||||
class WC_REST_Payments_Terminal_Locations_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
const STORE_LOCATIONS_TRANSIENT_KEY = 'wcpay_store_terminal_locations';
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/terminal/locations';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
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::CREATABLE,
|
||||
'callback' => [ $this, 'update_location' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'display_name' => [
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
],
|
||||
'address' => [
|
||||
'type' => 'object',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
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,
|
||||
[
|
||||
'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' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get store terminal location.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_store_location( $request ) {
|
||||
$store_address = WC()->countries;
|
||||
$location_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(),
|
||||
]
|
||||
);
|
||||
|
||||
// If address is not populated, emit an error and specify the URL where this can be done.
|
||||
// See also https://tosbourn.com/list-of-countries-without-a-postcode/ when launching in new countries.
|
||||
$is_address_populated = isset( $location_address['country'], $location_address['city'], $location_address['postal_code'], $location_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 {
|
||||
// Check the existing locations to see if one of them matches the store.
|
||||
// Originally we picked `get_bloginfo` for generating names, but later switched to `site_url` for max immutability.
|
||||
$store_hostname = str_replace( [ 'https://', 'http://' ], '', get_site_url() );
|
||||
$possible_names = [ get_bloginfo(), $store_hostname ];
|
||||
foreach ( $this->fetch_locations() as $location ) {
|
||||
if ( in_array( $location['display_name'], $possible_names, true ) ) {
|
||||
$matching_address_fields = array_intersect( $location['address'], $location_address );
|
||||
if ( count( $matching_address_fields ) === count( $location_address ) ) {
|
||||
return rest_ensure_response( $this->extract_location_fields( $location ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the location is missing, Create a new one and actualize the transient.
|
||||
$location = $this->api_client->create_terminal_location( $store_hostname, $location_address );
|
||||
$this->reload_locations();
|
||||
|
||||
return rest_ensure_response( $this->extract_location_fields( $location ) );
|
||||
} catch ( API_Exception $e ) {
|
||||
$error = new WP_Error( $e->get_error_code(), $e->getMessage() );
|
||||
// Stripe will return a 400 for incorrect city, state, or country. Ideally, we should return
|
||||
// a more appropriate error code like 'store_address_is_incorrect', but that will break older mobile app clients.
|
||||
// Until we have a more granular versioning support for WCPay REST endpoints, this is the best we can do.
|
||||
if ( 'invalid_request_error' === $e->get_error_code() ) {
|
||||
$error = new WP_Error(
|
||||
'store_address_is_incomplete',
|
||||
admin_url(
|
||||
add_query_arg(
|
||||
[
|
||||
'page' => 'wc-settings',
|
||||
'tab' => 'general',
|
||||
],
|
||||
'admin.php'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return rest_ensure_response( $error );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies the delete location request to the server.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function delete_location( $request ) {
|
||||
try {
|
||||
// Delete the location and reload the transient.
|
||||
$location = $this->api_client->delete_terminal_location( $request->get_param( 'location_id' ) );
|
||||
$this->reload_locations();
|
||||
|
||||
return rest_ensure_response( $location );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies the update location request to the server.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function update_location( $request ) {
|
||||
try {
|
||||
// Update the location and reload the transient.
|
||||
$location = $this->api_client->update_terminal_location( $request->get_param( 'location_id' ), $request['display_name'], $request['address'] );
|
||||
$this->reload_locations();
|
||||
|
||||
return rest_ensure_response( $this->extract_location_fields( $location ) );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies the get location request to the server.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_location( $request ) {
|
||||
try {
|
||||
// Check if the location is already in the transient.
|
||||
$location_id = $request->get_param( 'location_id' );
|
||||
foreach ( $this->fetch_locations() as $location ) {
|
||||
if ( $location['id'] === $location_id ) {
|
||||
return rest_ensure_response( $this->extract_location_fields( $location ) );
|
||||
}
|
||||
}
|
||||
// If the location is missing, fetch it individually and reload the transient.
|
||||
$request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API, $location_id );
|
||||
$request->assign_hook( 'wcpay_get_terminal_location' );
|
||||
$location = $request->send();
|
||||
$this->reload_locations();
|
||||
|
||||
return rest_ensure_response( $this->extract_location_fields( $location ) );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies the create location request to the server.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function create_location( $request ) {
|
||||
try {
|
||||
// Create location and reload the transient.
|
||||
$location = $this->api_client->create_terminal_location( $request['display_name'], $request['address'] );
|
||||
$this->reload_locations();
|
||||
|
||||
return rest_ensure_response( $this->extract_location_fields( $location ) );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies the get all locations request to the server.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_all_locations( $request ) {
|
||||
try {
|
||||
return rest_ensure_response( array_map( [ $this, 'extract_location_fields' ], $this->fetch_locations() ) );
|
||||
} catch ( API_Exception $e ) {
|
||||
return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the relevant fields from a terminal location object.
|
||||
*
|
||||
* @param array $location The location.
|
||||
* @return array The picked fields from location object.
|
||||
*/
|
||||
private function extract_location_fields( array $location ): array {
|
||||
return [
|
||||
'id' => $location['id'],
|
||||
'address' => $location['address'],
|
||||
'display_name' => $location['display_name'],
|
||||
'livemode' => $location['livemode'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read locations from transient and re-populates it if needed.
|
||||
*
|
||||
* @return array Terminal locations.
|
||||
* @throws API_Exception If request to server fails.
|
||||
*/
|
||||
private function fetch_locations(): array {
|
||||
$locations = get_transient( static::STORE_LOCATIONS_TRANSIENT_KEY );
|
||||
if ( ! $locations ) {
|
||||
$request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API );
|
||||
$request->assign_hook( 'wcpay_get_terminal_locations' );
|
||||
$locations = $request->send();
|
||||
set_transient( static::STORE_LOCATIONS_TRANSIENT_KEY, $locations, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the locations stored in transient.
|
||||
*
|
||||
* @return void
|
||||
* @throws API_Exception If request to server fails.
|
||||
*/
|
||||
private function reload_locations() {
|
||||
$request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API );
|
||||
$request->assign_hook( 'wcpay_get_terminal_locations' );
|
||||
|
||||
$locations = $request->send();
|
||||
set_transient( static::STORE_LOCATIONS_TRANSIENT_KEY, $locations, DAY_IN_SECONDS );
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Timeline_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for the timeline, which includes all events related to an intention.
|
||||
*/
|
||||
class WC_REST_Payments_Timeline_Controller extends WC_Payments_REST_Controller {
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/timeline';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<intention_id>\w+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_timeline' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve timeline to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_timeline( $request ) {
|
||||
$intention_id = $request->get_param( 'intention_id' );
|
||||
return $this->forward_request( 'get_timeline', [ $intention_id ] );
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Tos_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Add_Account_Tos_Agreement;
|
||||
use WCPay\Exceptions\Rest_Request_Exception;
|
||||
use WCPay\Logger;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for Terms of Services routes.
|
||||
*/
|
||||
class WC_REST_Payments_Tos_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Result codes for returning to the WCPay server API. They don't have any special meaning, but can will be logged
|
||||
* and are therefore useful when debugging how we reacted to a webhook.
|
||||
*/
|
||||
const RESULT_SUCCESS = 'success';
|
||||
const RESULT_BAD_REQUEST = 'bad_request';
|
||||
const RESULT_ERROR = 'error';
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/tos';
|
||||
|
||||
/**
|
||||
* Instance of WC_Payment_Gateway_WCPay
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* WC Payments Account.
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC_REST_Payments_Webhook_Controller constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WC_Payments_API_Client instance.
|
||||
* @param WC_Payment_Gateway_WCPay $gateway WC_Payment_Gateway_WCPay instance.
|
||||
* @param WC_Payments_Account $account WC_Payments_Account instance.
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Account $account ) {
|
||||
parent::__construct( $api_client );
|
||||
$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::CREATABLE,
|
||||
'callback' => [ $this, 'handle_tos' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/reactivate',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'reactivate' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/stripe_track_connected',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'remove_stripe_connect_track' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record ToS acceptance.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
* @throws Rest_Request_Exception Throw if accept param is missing.
|
||||
*/
|
||||
public function handle_tos( $request ) {
|
||||
$body = $request->get_json_params();
|
||||
|
||||
try {
|
||||
if ( ! isset( $body['accept'] ) ) {
|
||||
throw new Rest_Request_Exception( __( 'ToS accept parameter is missing', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
$is_accepted = (bool) $body['accept'];
|
||||
|
||||
Logger::debug( sprintf( 'ToS acceptance request received. Accept: %s', $is_accepted ? 'yes' : 'no' ) );
|
||||
|
||||
if ( $is_accepted ) {
|
||||
$this->handle_tos_accepted();
|
||||
} else {
|
||||
$this->handle_tos_declined();
|
||||
}
|
||||
} catch ( Rest_Request_Exception $e ) {
|
||||
Logger::error( $e );
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_BAD_REQUEST ], 400 );
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( $e );
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_ERROR ], 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process ToS accepted.
|
||||
*/
|
||||
private function handle_tos_accepted() {
|
||||
$this->gateway->enable();
|
||||
|
||||
// Accessing directly, because a user must be already logged in.
|
||||
$current_user = wp_get_current_user();
|
||||
$user_name = $current_user->user_login;
|
||||
|
||||
$request = Add_Account_Tos_Agreement::create();
|
||||
$request->set_source( 'settings-popup' );
|
||||
$request->set_user_name( $user_name );
|
||||
$request->send();
|
||||
|
||||
$this->account->refresh_account_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process ToS declined.
|
||||
*/
|
||||
private function handle_tos_declined() {
|
||||
// TODO: maybe record ToS declined data.
|
||||
$this->gateway->disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the gateway again, after it's been disabled.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function reactivate( $request ) {
|
||||
try {
|
||||
$this->gateway->enable();
|
||||
Logger::debug( 'Gateway re-enabled after ToS decline.' );
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( $e );
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_ERROR ], 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes _wcpay_onboarding_stripe_connected option after KYC completion has been tracked.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function remove_stripe_connect_track( $request ) {
|
||||
delete_option( '_wcpay_onboarding_stripe_connected' );
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] );
|
||||
}
|
||||
}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Transactions_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\List_Transactions;
|
||||
use WCPay\Core\Server\Request\List_Fraud_Outcome_Transactions;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for transactions.
|
||||
*/
|
||||
class WC_REST_Payments_Transactions_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/transactions';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_transactions' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/download',
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_transactions_export' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_transactions_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/search',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_transactions_search_autocomplete' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/fraud-outcomes',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_fraud_outcome_transactions' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/fraud-outcomes/summary',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_fraud_outcome_transactions_summary' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/fraud-outcomes/search',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_fraud_outcome_transactions_search_autocomplete' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/fraud-outcomes/download',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_fraud_outcome_transactions_export' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_transactions( $request ) {
|
||||
|
||||
$wcpay_request = List_Transactions::from_rest_request( $request );
|
||||
|
||||
return $wcpay_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve fraud outcome transactions to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_fraud_outcome_transactions( $request ) {
|
||||
$wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request );
|
||||
|
||||
return $this->forward_request( 'list_fraud_outcome_transactions', [ $wcpay_request ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve fraud outcome transactions summary to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_fraud_outcome_transactions_summary( $request ) {
|
||||
$wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request );
|
||||
|
||||
return $this->forward_request( 'list_fraud_outcome_transactions_summary', [ $wcpay_request ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions search options to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_fraud_outcome_transactions_search_autocomplete( $request ) {
|
||||
$wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request );
|
||||
|
||||
return $this->forward_request( 'get_fraud_outcome_transactions_search_autocomplete', [ $wcpay_request ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate transactions export via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_fraud_outcome_transactions_export( $request ) {
|
||||
$wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request );
|
||||
|
||||
return $this->forward_request( 'get_fraud_outcome_transactions_export', [ $wcpay_request ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate transactions export via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_transactions_export( $request ) {
|
||||
$user_email = $request->get_param( 'user_email' );
|
||||
$deposit_id = $request->get_param( 'deposit_id' );
|
||||
$locale = $request->get_param( 'locale' );
|
||||
$filters = $this->get_transactions_filters( $request );
|
||||
|
||||
return $this->forward_request( 'get_transactions_export', [ $filters, $user_email, $deposit_id, $locale ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions summary to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_transactions_summary( $request ) {
|
||||
$deposit_id = $request->get_param( 'deposit_id' );
|
||||
$filters = $this->get_transactions_filters( $request );
|
||||
return $this->forward_request( 'get_transactions_summary', [ $filters, $deposit_id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions search options to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function get_transactions_search_autocomplete( $request ) {
|
||||
$search_term = $request->get_param( 'search_term' );
|
||||
return $this->forward_request( 'get_transactions_search_autocomplete', [ $search_term ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract transactions filters from request
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
private function get_transactions_filters( $request ) {
|
||||
$date_between_filter = $request->get_param( 'date_between' );
|
||||
$user_timezone = $request->get_param( 'user_timezone' );
|
||||
|
||||
if ( ! is_null( $date_between_filter ) ) {
|
||||
$date_between_filter = array_map(
|
||||
function ( $transaction_date ) use ( $user_timezone ) {
|
||||
return $this->format_transaction_date_with_timestamp( $transaction_date, $user_timezone );
|
||||
},
|
||||
$date_between_filter
|
||||
);
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
[
|
||||
'match' => $request->get_param( 'match' ),
|
||||
'date_before' => $this->format_transaction_date_with_timestamp( $request->get_param( 'date_before' ), $user_timezone ),
|
||||
'date_after' => $this->format_transaction_date_with_timestamp( $request->get_param( 'date_after' ), $user_timezone ),
|
||||
'date_between' => $date_between_filter,
|
||||
'type_is' => $request->get_param( 'type_is' ),
|
||||
'type_is_not' => $request->get_param( 'type_is_not' ),
|
||||
'source_device_is' => $request->get_param( 'source_device_is' ),
|
||||
'source_device_is_not' => $request->get_param( 'source_device_is_not' ),
|
||||
'channel_is' => $request->get_param( 'channel_is' ),
|
||||
'channel_is_not' => $request->get_param( 'channel_is_not' ),
|
||||
'customer_country_is' => $request->get_param( 'customer_country_is' ),
|
||||
'customer_country_is_not' => $request->get_param( 'customer_country_is_not' ),
|
||||
'risk_level_is' => $request->get_param( 'risk_level_is' ),
|
||||
'risk_level_is_not' => $request->get_param( 'risk_level_is_not' ),
|
||||
'store_currency_is' => $request->get_param( 'store_currency_is' ),
|
||||
'customer_currency_is' => $request->get_param( 'customer_currency_is' ),
|
||||
'customer_currency_is_not' => $request->get_param( 'customer_currency_is_not' ),
|
||||
'source_is' => $request->get_param( 'source_is' ),
|
||||
'source_is_not' => $request->get_param( 'source_is_not' ),
|
||||
'loan_id_is' => $request->get_param( 'loan_id_is' ),
|
||||
'search' => $request->get_param( 'search' ),
|
||||
],
|
||||
static function ( $filter ) {
|
||||
return null !== $filter;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the incoming transaction date as per the blog's timezone.
|
||||
*
|
||||
* @param string|null $transaction_date Transaction date to format.
|
||||
* @param string|null $user_timezone User's timezone passed from client.
|
||||
*
|
||||
* @return string|null The formatted transaction date as per timezone.
|
||||
*/
|
||||
private function format_transaction_date_with_timestamp( $transaction_date, $user_timezone ) {
|
||||
if ( is_null( $transaction_date ) || is_null( $user_timezone ) ) {
|
||||
return $transaction_date;
|
||||
}
|
||||
|
||||
// Get blog timezone.
|
||||
$blog_time = new DateTime( $transaction_date );
|
||||
$blog_time->setTimezone( new DateTimeZone( wp_timezone_string() ) );
|
||||
|
||||
// Get local timezone.
|
||||
$local_time = new DateTime( $transaction_date );
|
||||
$local_time->setTimezone( new DateTimeZone( $user_timezone ) );
|
||||
|
||||
// Compute time difference in minutes.
|
||||
$time_difference = ( strtotime( $local_time->format( 'Y-m-d H:i:s' ) ) - strtotime( $blog_time->format( 'Y-m-d H:i:s' ) ) ) / 60;
|
||||
|
||||
// Shift date by time difference.
|
||||
$formatted_date = new DateTime( $transaction_date );
|
||||
date_modify( $formatted_date, $time_difference . 'minutes' );
|
||||
|
||||
return $formatted_date->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_VAT_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for vat.
|
||||
*/
|
||||
class WC_REST_Payments_VAT_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/vat';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<vat_number>[\w\.\%]+)',
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'validate_vat' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'args' => [
|
||||
'vat_number' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => false,
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => true,
|
||||
],
|
||||
'address' => [
|
||||
'type' => 'string',
|
||||
'format' => 'textarea-field',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'save_vat_details' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate VAT number to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function validate_vat( $request ) {
|
||||
$vat_number = sanitize_text_field( $request->get_param( 'vat_number' ) );
|
||||
$server_request = Request::get( WC_Payments_API_Client::VAT_API, $vat_number );
|
||||
$server_request->assign_hook( 'wcpay_validate_vat_request' );
|
||||
return $server_request->handle_rest_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save VAT details and respond via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function save_vat_details( $request ) {
|
||||
$vat_number = $request->get_param( 'vat_number' );
|
||||
$name = $request->get_param( 'name' );
|
||||
$address = $request->get_param( 'address' );
|
||||
return $this->forward_request( 'save_vat_details', [ $vat_number, $name, $address ] );
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_Payments_Webhook_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
use WCPay\Exceptions\Invalid_Webhook_Data_Exception;
|
||||
use WCPay\Logger;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for webhooks.
|
||||
*/
|
||||
class WC_REST_Payments_Webhook_Controller extends WC_Payments_REST_Controller {
|
||||
|
||||
/**
|
||||
* Result codes for returning to the WCPay server API. They don't have any special meaning, but can will be logged
|
||||
* and are therefore useful when debugging how we reacted to a webhook.
|
||||
*/
|
||||
const RESULT_SUCCESS = 'success';
|
||||
const RESULT_BAD_REQUEST = 'bad_request';
|
||||
const RESULT_ERROR = 'error';
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/webhook';
|
||||
|
||||
/**
|
||||
* Webhook Processing Service.
|
||||
*
|
||||
* @var WC_Payments_Webhook_Processing_Service
|
||||
*/
|
||||
private $webhook_processing_service;
|
||||
|
||||
/**
|
||||
* WC_REST_Payments_Webhook_Controller constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WC_Payments_API_Client instance.
|
||||
* @param WC_Payments_Webhook_Processing_Service $webhook_processing_service WC_Payments_Webhook_Processing_Service instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $api_client,
|
||||
WC_Payments_Webhook_Processing_Service $webhook_processing_service
|
||||
) {
|
||||
parent::__construct( $api_client );
|
||||
$this->webhook_processing_service = $webhook_processing_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'handle_webhook' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions to respond with via API.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function handle_webhook( $request ) {
|
||||
$body = $request->get_json_params();
|
||||
|
||||
try {
|
||||
$this->webhook_processing_service->process( $body );
|
||||
} catch ( Invalid_Webhook_Data_Exception $e ) {
|
||||
Logger::error( $e );
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_BAD_REQUEST ], 400 );
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( $e );
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_ERROR ], 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] );
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_UPE_Flag_Toggle_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
/**
|
||||
* REST controller for UPE feature flag. Needs to stay in the codebase to avoid error on plugin update for versions 6.9.2 or earlier.
|
||||
*/
|
||||
class WC_REST_UPE_Flag_Toggle_Controller extends WP_REST_Controller {
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_REST_WooPay_Session_Controller
|
||||
*
|
||||
* @package WooCommerce\Payments\Admin
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use WCPay\WooPay\WooPay_Session;
|
||||
use Automattic\Jetpack\Connection\Rest_Authentication;
|
||||
use WCPay\Logger;
|
||||
|
||||
/**
|
||||
* REST controller to check get WooPay extension data for user.
|
||||
*/
|
||||
class WC_REST_WooPay_Session_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'payments/woopay';
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'session';
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_session_data' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
'args' => [
|
||||
'email' => [
|
||||
'type' => 'string',
|
||||
'format' => 'email',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve WooPay session data.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return WP_Error|WP_REST_Response The initial session request data.
|
||||
*/
|
||||
public function get_session_data( WP_REST_Request $request ): WP_REST_Response {
|
||||
try {
|
||||
// phpcs:ignore
|
||||
/**
|
||||
* @psalm-suppress UndefinedClass
|
||||
*/
|
||||
$response = WooPay_Session::get_init_session_request( null, null, null, $request );
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
} catch ( Exception $e ) {
|
||||
$error = new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 400 ] );
|
||||
Logger::log( 'Error validating cart token from WooPay request: ' . $e->getMessage() );
|
||||
|
||||
return rest_convert_error_to_response( $error );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permission confirms that the request is from WooPay.
|
||||
*
|
||||
* @return bool True if request is from WooPay and has a valid signature.
|
||||
*/
|
||||
public function check_permission() {
|
||||
return $this->is_request_from_woopay() && $this->has_valid_request_signature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request that's currently being processed is signed with the blog token.
|
||||
*
|
||||
* @return bool True if the request signature is valid.
|
||||
*/
|
||||
private function has_valid_request_signature(): bool {
|
||||
return apply_filters( 'wcpay_woopay_is_signed_with_blog_token', Rest_Authentication::is_signed_with_blog_token() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request that's currently being processed is from WooPay, false
|
||||
* otherwise.
|
||||
*
|
||||
* @return bool True if request is from WooPay.
|
||||
*/
|
||||
private function is_request_from_woopay(): bool {
|
||||
return isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT'];
|
||||
}
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Task_Disputes
|
||||
*
|
||||
* @package WooCommerce\Payments\Tasks
|
||||
*/
|
||||
|
||||
namespace WooCommerce\Payments\Tasks;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
|
||||
use WCPay\Database_Cache;
|
||||
use WC_Payments_Utils;
|
||||
use WC_Payments_API_Client;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC Onboarding Task displayed if disputes awaiting response.
|
||||
*
|
||||
* Note: this task is separate to the Payments → Overview disputes task, which is defined in client/overview/task-list/tasks.js.
|
||||
*/
|
||||
class WC_Payments_Task_Disputes extends Task {
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $api_client;
|
||||
|
||||
/**
|
||||
* Database_Cache instance.
|
||||
*
|
||||
* @var Database_Cache
|
||||
*/
|
||||
private $database_cache;
|
||||
|
||||
|
||||
/**
|
||||
* Disputes due within 7 days.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $disputes_due_within_7d;
|
||||
|
||||
/**
|
||||
* Disputes due within 1 day.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $disputes_due_within_1d;
|
||||
|
||||
/**
|
||||
* A memory cache of all disputes needing response.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $disputes_needing_response = null;
|
||||
|
||||
/**
|
||||
* WC_Payments_Task_Disputes constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
$this->api_client = \WC_Payments::get_payments_api_client();
|
||||
$this->database_cache = \WC_Payments::get_database_cache();
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the task.
|
||||
*/
|
||||
private function fetch_relevant_disputes() {
|
||||
$this->disputes_due_within_7d = $this->get_disputes_needing_response_within_days( 7 );
|
||||
$this->disputes_due_within_1d = $this->get_disputes_needing_response_within_days( 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the task ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id() {
|
||||
return 'woocommerce_payments_disputes_task';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the task title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_title() {
|
||||
if ( null === $this->disputes_needing_response ) {
|
||||
$this->fetch_relevant_disputes();
|
||||
}
|
||||
if ( count( (array) $this->disputes_due_within_7d ) === 1 ) {
|
||||
$dispute = $this->disputes_due_within_7d[0];
|
||||
$amount = WC_Payments_Utils::interpret_stripe_amount( $dispute['amount'], $dispute['currency'] );
|
||||
$amount_formatted = WC_Payments_Utils::format_currency( $amount, $dispute['currency'] );
|
||||
if ( count( (array) $this->disputes_due_within_1d ) > 0 ) {
|
||||
return sprintf(
|
||||
/* translators: %s is a currency formatted amount */
|
||||
__( 'Respond to a dispute for %s – Last day', 'woocommerce-payments' ),
|
||||
$amount_formatted
|
||||
);
|
||||
}
|
||||
return sprintf(
|
||||
/* translators: %s is a currency formatted amount */
|
||||
__( 'Respond to a dispute for %s', 'woocommerce-payments' ),
|
||||
$amount_formatted
|
||||
);
|
||||
}
|
||||
|
||||
$active_disputes = $this->get_disputes_needing_response();
|
||||
if ( ! is_array( $active_disputes ) || count( $active_disputes ) === 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$dispute_currencies = array_unique( array_column( $active_disputes, 'currency' ) );
|
||||
|
||||
// If multiple currencies, use simple task title without total amounts.
|
||||
if ( count( $dispute_currencies ) > 1 ) {
|
||||
return sprintf(
|
||||
// translators: %d is a number greater than 1.
|
||||
__( 'Respond to %d active disputes', 'woocommerce-payments' ),
|
||||
count( $active_disputes )
|
||||
);
|
||||
}
|
||||
|
||||
// If single currency, calculate total amount and include in task title.
|
||||
$dispute_total = array_reduce(
|
||||
$active_disputes,
|
||||
function ( $total, $dispute ) {
|
||||
return $total + ( $dispute['amount'] ?? 0 );
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
$dispute_total_formatted = WC_Payments_Utils::format_currency(
|
||||
WC_Payments_Utils::interpret_stripe_amount( $dispute_total, $dispute_currencies[0] ),
|
||||
$dispute_currencies[0]
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
/* translators: %d is a number greater than 1. %s is a formatted amount, eg: $10.00 */
|
||||
__( 'Respond to %1$d active disputes for a total of %2$s', 'woocommerce-payments' ),
|
||||
count( $active_disputes ),
|
||||
$dispute_total_formatted
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent list ID.
|
||||
*
|
||||
* This function prior to WC 6.4.0 was abstract and so is needed for backwards compatibility.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_parent_id() {
|
||||
// WC 6.4.0 compatibility.
|
||||
if ( is_callable( 'parent::get_parent_id' ) ) {
|
||||
return parent::get_parent_id();
|
||||
}
|
||||
|
||||
return 'extended';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the task subtitle.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_additional_info() {
|
||||
if ( count( (array) $this->disputes_due_within_7d ) === 1 ) {
|
||||
$local_timezone = new \DateTimeZone( wp_timezone_string() );
|
||||
$dispute = $this->disputes_due_within_7d[0];
|
||||
$due_by_local_time = ( new \DateTime( $dispute['due_by'] ) )->setTimezone( $local_timezone );
|
||||
// Sum of Unix timestamp and timezone offset in seconds.
|
||||
$due_by_ts = $due_by_local_time->getTimestamp() + $due_by_local_time->getOffset();
|
||||
|
||||
if ( count( (array) $this->disputes_due_within_1d ) > 0 ) {
|
||||
return sprintf(
|
||||
/* translators: %s is time, eg: 11:59 PM */
|
||||
__( 'Respond today by %s', 'woocommerce-payments' ),
|
||||
date_i18n( wc_time_format(), $due_by_ts )
|
||||
);
|
||||
}
|
||||
|
||||
$now = new \DateTime( 'now', $local_timezone );
|
||||
$diff = $now->diff( $due_by_local_time );
|
||||
|
||||
return sprintf(
|
||||
/* translators: %1$s is a date, eg: Jan 1, 2021. %2$s is the number of days left, eg: 2 days. */
|
||||
__( 'By %1$s – %2$s left to respond', 'woocommerce-payments' ),
|
||||
date_i18n( wc_date_format(), $due_by_ts ),
|
||||
/* translators: %s is the number of days left, e.g. 1 day. */
|
||||
sprintf( _n( '%d day', '%d days', $diff->days, 'woocommerce-payments' ), $diff->days )
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( (array) $this->disputes_due_within_1d ) > 0 ) {
|
||||
return sprintf(
|
||||
/* translators: %d is the number of disputes. */
|
||||
__(
|
||||
'Final day to respond to %d of the disputes',
|
||||
'woocommerce-payments'
|
||||
),
|
||||
count( (array) $this->disputes_due_within_1d )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
/* translators: %d is the number of disputes. */
|
||||
__(
|
||||
'Last week to respond to %d of the disputes',
|
||||
'woocommerce-payments'
|
||||
),
|
||||
count( (array) $this->disputes_due_within_7d )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the task's action URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_action_url() {
|
||||
$disputes = $this->disputes_due_within_7d;
|
||||
if ( count( (array) $disputes ) === 1 ) {
|
||||
$dispute = $disputes[0];
|
||||
return admin_url(
|
||||
add_query_arg(
|
||||
[
|
||||
'page' => 'wc-admin',
|
||||
'path' => '%2Fpayments%2Ftransactions%2Fdetails',
|
||||
'id' => $dispute['charge_id'],
|
||||
],
|
||||
'admin.php'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return admin_url(
|
||||
add_query_arg(
|
||||
[
|
||||
'page' => 'wc-admin',
|
||||
'path' => '%2Fpayments%2Fdisputes',
|
||||
'filter' => 'awaiting_response',
|
||||
],
|
||||
'admin.php'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated time to complete the task.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_time() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the task content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the task is completed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_complete() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the task is visible.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_view() {
|
||||
if ( null === $this->disputes_needing_response ) {
|
||||
$this->fetch_relevant_disputes();
|
||||
}
|
||||
return count( (array) $this->disputes_due_within_7d ) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disputes needing response within the given number of days.
|
||||
*
|
||||
* @param int $num_days Number of days in the future to check for disputes needing response.
|
||||
*
|
||||
* @return array Disputes needing response within the given number of days.
|
||||
*/
|
||||
private function get_disputes_needing_response_within_days( $num_days ) {
|
||||
$to_return = [];
|
||||
|
||||
$active_disputes = $this->get_disputes_needing_response();
|
||||
if ( ! is_array( $active_disputes ) ) {
|
||||
return $to_return;
|
||||
}
|
||||
|
||||
foreach ( $active_disputes as $dispute ) {
|
||||
if ( ! $dispute['due_by'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare UTC times.
|
||||
$now_utc = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
|
||||
$due_by_utc = new \DateTime( $dispute['due_by'], new \DateTimeZone( 'UTC' ) );
|
||||
|
||||
if ( $now_utc > $due_by_utc ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $now_utc->diff( $due_by_utc );
|
||||
// If the dispute is due within the given number of days, add it to the list.
|
||||
if ( $diff->days <= $num_days ) {
|
||||
$to_return[] = $dispute;
|
||||
}
|
||||
}
|
||||
|
||||
return $to_return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets disputes awaiting a response. ie have a 'needs_response' or 'warning_needs_response' status.
|
||||
*
|
||||
* @return array|null Array of disputes awaiting a response. Null on failure.
|
||||
*/
|
||||
private function get_disputes_needing_response() {
|
||||
if ( null !== $this->disputes_needing_response ) {
|
||||
return $this->disputes_needing_response;
|
||||
}
|
||||
|
||||
$this->disputes_needing_response = $this->database_cache->get_or_add(
|
||||
Database_Cache::ACTIVE_DISPUTES_KEY,
|
||||
function () {
|
||||
try {
|
||||
$response = $this->api_client->get_disputes(
|
||||
[
|
||||
'pagesize' => 50,
|
||||
'search' => [ 'warning_needs_response', 'needs_response' ],
|
||||
]
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
// Ensure an array is always returned, even if the API call fails.
|
||||
return [];
|
||||
}
|
||||
|
||||
$active_disputes = $response['data'] ?? [];
|
||||
|
||||
// sort by due_by date ascending.
|
||||
usort(
|
||||
$active_disputes,
|
||||
function ( $a, $b ) {
|
||||
$a_due_by = new \DateTime( $a['due_by'] );
|
||||
$b_due_by = new \DateTime( $b['due_by'] );
|
||||
|
||||
return $a_due_by <=> $b_due_by;
|
||||
}
|
||||
);
|
||||
|
||||
return $active_disputes;
|
||||
},
|
||||
'is_array'
|
||||
);
|
||||
|
||||
return $this->disputes_needing_response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Tracker
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* An API for adding track events that will get unloaded
|
||||
* at a later stage and pushed to WP.com.
|
||||
*/
|
||||
class Tracker {
|
||||
/**
|
||||
* A key value array event_name => properties.
|
||||
*
|
||||
* @var array $admin_events
|
||||
*/
|
||||
protected static $admin_events = [];
|
||||
|
||||
/**
|
||||
* Record an event in Tracks
|
||||
*
|
||||
* This only sends track events for admin logged in users. This is a limitation of the
|
||||
* WC_Tracks and related classes.
|
||||
*
|
||||
* The event name will be prefixed before sending it see Core_Tracks_Wrapper.
|
||||
*
|
||||
* @param string $event_name The name of the event.
|
||||
* @param array $properties Custom properties to send with the event.
|
||||
*/
|
||||
public static function track_admin( $event_name, $properties = [] ) {
|
||||
self::$admin_events[ $event_name ] = $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a track event.
|
||||
*
|
||||
* @param string $event_name The name of the event that should be removed.
|
||||
*/
|
||||
public static function remove_admin_event( $event_name ) {
|
||||
if ( isset( self::$admin_events ) ) {
|
||||
unset( self::$admin_events[ $event_name ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a track event.
|
||||
*/
|
||||
public static function get_admin_events() {
|
||||
return self::$admin_events;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Core Tracks Wrapper
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use WC_Tracks;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* Moves all events from Tracker to Tracks loader
|
||||
*/
|
||||
function record_tracker_events() {
|
||||
foreach ( Tracker::get_admin_events() as $event => $properties ) {
|
||||
WC_Tracks::record_event( $event, $properties );
|
||||
Tracker::remove_admin_event( $event );
|
||||
}
|
||||
}
|
||||
|
||||
// Loaded on admin_init to ensure that we are in admin and that WC_Tracks is loaded.
|
||||
add_action(
|
||||
'admin_init',
|
||||
function () {
|
||||
if ( ! class_exists( 'WC_Tracks' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move all events with priority 1 just before the admin_footer hook adds footer pixels.
|
||||
add_action( 'admin_footer', 'WCPay\record_tracker_events', 1 );
|
||||
|
||||
/**
|
||||
* Send all events that were not handled in `admin_footer`.
|
||||
*
|
||||
* Between shutdown and admin footer many things can happen. Admin footer loads
|
||||
* scripts in the markup images that will call tracks from the browsers
|
||||
* side (which means it's faster as we're not doing network calls to wp.com server
|
||||
* side). Anything that is added afterward must be sent from the
|
||||
* server, but doing it on shutdown means it's not blocking anything.
|
||||
*/
|
||||
add_action( 'shutdown', __NAMESPACE__ . '\\record_tracker_events', 1 );
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Compatibility_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use WC_Payments;
|
||||
use WC_Payments_API_Client;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* Class to send compatibility data to the server.
|
||||
*/
|
||||
class Compatibility_Service {
|
||||
const UPDATE_COMPATIBILITY_DATA = 'wcpay_update_compatibility_data';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* Constructor for Compatibility_Service.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client.
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $payments_api_client ) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'woocommerce_payments_account_refreshed', [ $this, 'update_compatibility_data' ] );
|
||||
add_action( 'after_switch_theme', [ $this, 'update_compatibility_data' ] );
|
||||
add_filter( 'wc_payments_get_onboarding_data_args', [ $this, 'add_compatibility_onboarding_data' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the sending of the compatibility data to send only the last update in T minutes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function update_compatibility_data() {
|
||||
// This will delete the previous compatibility requests in the last two minutes, and only send the last update to the server, ensuring there's only one update in two minutes.
|
||||
WC_Payments::get_action_scheduler_service()->schedule_job( time() + 2 * MINUTE_IN_SECONDS, self::UPDATE_COMPATIBILITY_DATA );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data we need to confirm compatibility and sends it to the server.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function update_compatibility_data_hook() {
|
||||
$this->payments_api_client->update_compatibility_data( $this->get_compatibility_data() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the compatibility data to the onboarding args.
|
||||
*
|
||||
* @param array $args The args being sent when onboarding.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_compatibility_onboarding_data( $args ): array {
|
||||
$args['compatibility_data'] = $this->get_compatibility_data();
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compatibility data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_compatibility_data(): array {
|
||||
$active_plugins = get_option( 'active_plugins', [] );
|
||||
$post_types_count = $this->get_post_types_count();
|
||||
$wc_permalinks = get_option( 'woocommerce_permalinks', [] );
|
||||
$wc_shop_permalink = $this->get_permalink_for_page_id( 'shop' );
|
||||
$wc_cart_permalink = $this->get_permalink_for_page_id( 'cart' );
|
||||
$wc_checkout_permalink = $this->get_permalink_for_page_id( 'checkout' );
|
||||
|
||||
return [
|
||||
'woopayments_version' => WCPAY_VERSION_NUMBER,
|
||||
'woocommerce_version' => WC_VERSION,
|
||||
'woocommerce_permalinks' => $wc_permalinks,
|
||||
'woocommerce_shop' => $wc_shop_permalink,
|
||||
'woocommerce_cart' => $wc_cart_permalink,
|
||||
'woocommerce_checkout' => $wc_checkout_permalink,
|
||||
'blog_theme' => get_stylesheet(),
|
||||
'active_plugins' => $active_plugins,
|
||||
'post_types_count' => $post_types_count,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of public posts for each post type.
|
||||
*
|
||||
* @return array<\WP_Post_Type|string, int>
|
||||
*/
|
||||
private function get_post_types_count(): array {
|
||||
$post_types = get_post_types(
|
||||
[
|
||||
'public' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$post_types_count = [];
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$post_types_count[ $post_type ] = (int) wp_count_posts( $post_type )->publish;
|
||||
}
|
||||
|
||||
return $post_types_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the permalink for a page ID.
|
||||
*
|
||||
* @param string $page_id The page ID to get the permalink for.
|
||||
*
|
||||
* @return string The permalink for the page ID, or 'Not set' if the permalink is not available.
|
||||
*/
|
||||
private function get_permalink_for_page_id( string $page_id ): string {
|
||||
$permalink = get_permalink( wc_get_page_id( $page_id ) );
|
||||
|
||||
return $permalink ? $permalink : 'Not set';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Database_Cache
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyCacheInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* A class for caching data as an option in the database.
|
||||
*/
|
||||
class Database_Cache implements MultiCurrencyCacheInterface {
|
||||
const ACCOUNT_KEY = 'wcpay_account_data';
|
||||
const ONBOARDING_FIELDS_DATA_KEY = 'wcpay_onboarding_fields_data';
|
||||
const BUSINESS_TYPES_KEY = 'wcpay_business_types_data';
|
||||
const PAYMENT_PROCESS_FACTORS_KEY = 'wcpay_payment_process_factors';
|
||||
const FRAUD_SERVICES_KEY = 'wcpay_fraud_services_data';
|
||||
const RECOMMENDED_PAYMENT_METHODS = 'wcpay_recommended_payment_methods';
|
||||
|
||||
/**
|
||||
* Refresh during AJAX calls is avoided, but white-listing
|
||||
* a key here will allow the refresh to happen.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
const AJAX_ALLOWED_KEYS = [
|
||||
self::PAYMENT_PROCESS_FACTORS_KEY,
|
||||
];
|
||||
|
||||
/**
|
||||
* Payment methods cache key prefix. Used in conjunction with the customer_id to cache a customer's payment methods.
|
||||
*/
|
||||
const PAYMENT_METHODS_KEY_PREFIX = 'wcpay_pm_';
|
||||
|
||||
/**
|
||||
* Dispute status counts cache key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const DISPUTE_STATUS_COUNTS_KEY = 'wcpay_dispute_status_counts_cache';
|
||||
|
||||
/**
|
||||
* Active disputes cache key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const ACTIVE_DISPUTES_KEY = 'wcpay_active_dispute_cache';
|
||||
|
||||
/**
|
||||
* Cache key for authorization summary data like count, total amount, etc.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTHORIZATION_SUMMARY_KEY = 'wcpay_authorization_summary_cache';
|
||||
|
||||
/**
|
||||
* Cache key for authorization summary data like count, total amount, etc in test mode.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AUTHORIZATION_SUMMARY_KEY_TEST_MODE = 'wcpay_test_authorization_summary_cache';
|
||||
|
||||
/**
|
||||
* Cache key for eligible connect incentive data.
|
||||
*/
|
||||
const CONNECT_INCENTIVE_KEY = 'wcpay_connect_incentive';
|
||||
|
||||
/**
|
||||
* Tracking info cache key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TRACKING_INFO_KEY = 'wcpay_tracking_info_cache';
|
||||
|
||||
/**
|
||||
* Refresh disabled flag, controlling the behaviour of the get_or_add function.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $refresh_disabled;
|
||||
|
||||
/**
|
||||
* In-memory cache for the duration of a single request.
|
||||
*
|
||||
* This is used to avoid multiple database reads for the same data and as a backstop in case the database write fails,
|
||||
* thus ensuring the cache generator is not called multiple times (which would mean multiple API calls to our platform).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $in_memory_cache = [];
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->refresh_disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'action_scheduler_before_execute', [ $this, 'disable_refresh' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from cache or regenerates and adds it to the cache.
|
||||
*
|
||||
* @param string $key The options key to cache the data under.
|
||||
* @param callable $generator Function/callable regenerating the missing value. If null or false is returned, it will be treated as an error.
|
||||
* @param callable $validate_data Function/callable validating the data after it is retrieved from the cache. If it returns false, the cache will be refreshed.
|
||||
* @param boolean $force_refresh Regenerates the cache regardless of its state if true.
|
||||
* @param boolean $refreshed Is set to true if the cache has been refreshed without errors and with a non-empty value.
|
||||
*
|
||||
* @return mixed|null The cached value. NULL on failure to regenerate or validate the data.
|
||||
*/
|
||||
public function get_or_add( string $key, callable $generator, callable $validate_data, bool $force_refresh = false, bool &$refreshed = false ) {
|
||||
$cache_contents = $this->get_from_cache( $key );
|
||||
$data = null;
|
||||
$old_data = null;
|
||||
|
||||
// If the stored data is valid, prepare it for return in case we don't need to refresh.
|
||||
// Also initialize old_data in case of errors.
|
||||
if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) && $validate_data( $cache_contents['data'] ) ) {
|
||||
$data = $cache_contents['data'];
|
||||
$old_data = $data;
|
||||
}
|
||||
|
||||
if ( $this->should_refresh_cache( $key, $cache_contents, $validate_data, $force_refresh ) ) {
|
||||
try {
|
||||
$data = $generator();
|
||||
$errored = ( false === $data || null === $data );
|
||||
} catch ( \Throwable $e ) {
|
||||
$errored = true;
|
||||
}
|
||||
|
||||
$refreshed = ! $errored;
|
||||
|
||||
if ( $errored ) {
|
||||
// Still return the old data on error and refresh the cache with it.
|
||||
$data = $old_data;
|
||||
}
|
||||
|
||||
$this->write_to_cache( $key, $data, $errored );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from the cache.
|
||||
*
|
||||
* @param string $key The key to look for.
|
||||
* @param bool $force If set, return from the cache without checking for expiry.
|
||||
*
|
||||
* @return mixed|null The cache contents. NULL if the cache is expired or missing.
|
||||
*/
|
||||
public function get( string $key, bool $force = false ) {
|
||||
$cache_contents = $this->get_from_cache( $key );
|
||||
if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) ) {
|
||||
if ( ! $force && $this->is_expired( $key, $cache_contents ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $cache_contents['data'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a value in the cache.
|
||||
*
|
||||
* @param string $key The key to store the value under.
|
||||
* @param mixed $data The value to store.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add( string $key, $data ) {
|
||||
$this->write_to_cache( $key, $data, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a value from the cache.
|
||||
*
|
||||
* @param string $key The key to delete.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete( string $key ) {
|
||||
// Remove from the in-memory cache.
|
||||
unset( $this->in_memory_cache[ $key ] );
|
||||
|
||||
// Remove from the DB cache.
|
||||
if ( delete_option( $key ) ) {
|
||||
// Clear the WP object cache to ensure the new data is fetched by other processes.
|
||||
wp_cache_delete( $key, 'options' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all cache entries that are keyed with a certain prefix.
|
||||
*
|
||||
* This is useful when you use dynamic cache keys.
|
||||
*
|
||||
* Note: Only key prefixes with known, static prefixes are allowed, for protection purposes.
|
||||
*
|
||||
* @param string $key_prefix The cache key prefix.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_by_prefix( string $key_prefix ) {
|
||||
// Protection against accidentally deleting all options or options that are not related to WCPay caching.
|
||||
// Feel free to update this statement as more prefix cache keys are used.
|
||||
if ( strncmp( $key_prefix, self::PAYMENT_METHODS_KEY_PREFIX, strlen( self::PAYMENT_METHODS_KEY_PREFIX ) ) !== 0 ) {
|
||||
return; // Maybe throw exception here...
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$options = $wpdb->get_results( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $key_prefix . '%' ) );
|
||||
foreach ( $options as $option ) {
|
||||
$this->delete( $option->option_name );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook function allowing the cache refresh to be selectively disabled in certain situations
|
||||
* (such as while running an Action Scheduler job). While the refresh is disabled, get_or_add
|
||||
* will only return the cached value and never regenerate it, even if it's expired.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disable_refresh() {
|
||||
$this->refresh_disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cache contents and, given the passed params and the current application state, determines whether the cache should be refreshed.
|
||||
* See get_or_add.
|
||||
*
|
||||
* @param string $key The cache key.
|
||||
* @param mixed $cache_contents The cache contents.
|
||||
* @param callable $validate_data Callback used to validate the cached data by the callee.
|
||||
* @param boolean $force_refresh Whether a refresh should be forced.
|
||||
*
|
||||
* @return boolean True if the cache needs to be refreshed.
|
||||
*/
|
||||
private function should_refresh_cache( string $key, $cache_contents, callable $validate_data, bool $force_refresh ): bool {
|
||||
// Always refresh if the flag is set.
|
||||
if ( $force_refresh ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not refresh if doing ajax or the refresh has been disabled (running an AS job).
|
||||
if (
|
||||
defined( 'DOING_CRON' )
|
||||
|| ( wp_doing_ajax() && ! in_array( $key, self::AJAX_ALLOWED_KEYS, true ) )
|
||||
|| $this->refresh_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The value of false means that there was never something cached.
|
||||
if ( false === $cache_contents ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-array, empty array, or missing expected fields mean corrupted data.
|
||||
// This also handles potential legacy data, which might have those keys missing.
|
||||
if ( ! is_array( $cache_contents )
|
||||
|| empty( $cache_contents )
|
||||
|| ! array_key_exists( 'data', $cache_contents )
|
||||
|| ! isset( $cache_contents['fetched'] )
|
||||
|| ! array_key_exists( 'errored', $cache_contents )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the data is not errored but invalid, we should refresh it.
|
||||
if ( ! $cache_contents['errored'] && ! $validate_data( $cache_contents['data'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh the expired data.
|
||||
if ( $this->is_expired( $key, $cache_contents ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache contents for a certain key.
|
||||
*
|
||||
* @param string $key The cache key.
|
||||
*
|
||||
* @return array|false The cache contents (array with `data`, `fetched`, and `errored` entries).
|
||||
* False if there is no cached data.
|
||||
*/
|
||||
private function get_from_cache( string $key ) {
|
||||
// Check the in-memory cache first.
|
||||
if ( isset( $this->in_memory_cache[ $key ] ) ) {
|
||||
return $this->in_memory_cache[ $key ];
|
||||
}
|
||||
|
||||
// Read from the DB cache.
|
||||
$data = get_option( $key );
|
||||
|
||||
// Store the data in the in-memory cache, including the case when there is no data cached (`false`).
|
||||
$this->in_memory_cache[ $key ] = $data;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the data in the cache metadata and stores it.
|
||||
*
|
||||
* @param string $key The key to store the data under.
|
||||
* @param mixed $data The data to store.
|
||||
* @param boolean $errored Whether the refresh operation resulted in an error before this has been called.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function write_to_cache( string $key, $data, bool $errored ) {
|
||||
// Add the data and expiry time to the array we're caching.
|
||||
$cache_contents = [];
|
||||
$cache_contents['data'] = $data;
|
||||
$cache_contents['fetched'] = time();
|
||||
$cache_contents['errored'] = $errored;
|
||||
|
||||
// Write the in-memory cache.
|
||||
$this->in_memory_cache[ $key ] = $cache_contents;
|
||||
|
||||
// Create or update the DB option cache.
|
||||
// Note: Since we are adding the current time to the option value, WP will ALWAYS write the option because
|
||||
// the cache contents value is different from the current one, even if the data is the same.
|
||||
// A `false` result ONLY means that the DB write failed.
|
||||
// Yes, there is the possibility that we attempt to write the same data multiple times within the SAME second,
|
||||
// and we will mistakenly think that the DB write failed. We are OK with this false positive,
|
||||
// since the actual data is the same.
|
||||
$result = update_option( $key, $cache_contents, 'no' );
|
||||
if ( false !== $result ) {
|
||||
// If the DB cache write succeeded, clear the WP object cache to ensure the new data is fetched by other processes.
|
||||
wp_cache_delete( $key, 'options' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cache contents are expired.
|
||||
*
|
||||
* @param string $key The cache key.
|
||||
* @param array $cache_contents The cache contents.
|
||||
*
|
||||
* @return boolean True if the contents are expired. False otherwise.
|
||||
*/
|
||||
private function is_expired( string $key, array $cache_contents ): bool {
|
||||
$ttl = $this->get_ttl( $key, $cache_contents );
|
||||
$expires = $cache_contents['fetched'] + $ttl;
|
||||
$now = time();
|
||||
|
||||
return $expires < $now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the key and the cache contents, and based on the application state, determines the cache TTL.
|
||||
*
|
||||
* @param string $key The cache key.
|
||||
* @param array $cache_contents The cache contents.
|
||||
*
|
||||
* @return integer The cache TTL.
|
||||
*/
|
||||
private function get_ttl( string $key, array $cache_contents ): int {
|
||||
switch ( $key ) {
|
||||
case self::ACCOUNT_KEY:
|
||||
if ( is_admin() ) {
|
||||
// Fetches triggered from the admin panel should be more frequent.
|
||||
if ( $cache_contents['errored'] ) {
|
||||
// Attempt to refresh the data quickly if the last fetch was an error.
|
||||
$ttl = 2 * MINUTE_IN_SECONDS;
|
||||
} else {
|
||||
// If the data was fetched successfully, fetch it every 2h.
|
||||
$ttl = 2 * HOUR_IN_SECONDS;
|
||||
}
|
||||
} else {
|
||||
// Non-admin requests should always refresh only after 24h since the last fetch.
|
||||
$ttl = DAY_IN_SECONDS;
|
||||
}
|
||||
break;
|
||||
case self::CURRENCIES_KEY:
|
||||
// Refresh the errored currencies quickly, otherwise cache for 6h.
|
||||
$ttl = $cache_contents['errored'] ? 2 * MINUTE_IN_SECONDS : 6 * HOUR_IN_SECONDS;
|
||||
break;
|
||||
case self::BUSINESS_TYPES_KEY:
|
||||
case self::ONBOARDING_FIELDS_DATA_KEY:
|
||||
// Cache these for a week.
|
||||
$ttl = WEEK_IN_SECONDS;
|
||||
break;
|
||||
case self::CONNECT_INCENTIVE_KEY:
|
||||
$ttl = $cache_contents['data']['ttl'] ?? HOUR_IN_SECONDS * 6;
|
||||
break;
|
||||
case self::PAYMENT_PROCESS_FACTORS_KEY:
|
||||
$ttl = 2 * HOUR_IN_SECONDS;
|
||||
break;
|
||||
case self::TRACKING_INFO_KEY:
|
||||
$ttl = $cache_contents['errored'] ? 2 * MINUTE_IN_SECONDS : MONTH_IN_SECONDS;
|
||||
break;
|
||||
default:
|
||||
// Default to 24h.
|
||||
$ttl = DAY_IN_SECONDS;
|
||||
break;
|
||||
}
|
||||
|
||||
return apply_filters( 'wcpay_database_cache_ttl', $ttl, $key, $cache_contents );
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Duplicate_Payment_Prevention_Service
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use Exception;
|
||||
use WC_Order;
|
||||
use WC_Payment_Gateway_WCPay;
|
||||
use WC_Payments_Order_Service;
|
||||
use WCPay\Constants\Intent_Status;
|
||||
use WCPay\Core\Server\Request\Get_Intention;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for methods, which detect existing payments or payment intents,
|
||||
* and prevent creating duplicate payments.
|
||||
*/
|
||||
class Duplicate_Payment_Prevention_Service {
|
||||
/**
|
||||
* Key name for saving the current processing order_id to WC Session with the purpose
|
||||
* of preventing duplicate payments in a single order.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const SESSION_KEY_PROCESSING_ORDER = 'wcpay_processing_order';
|
||||
|
||||
/**
|
||||
* Flag to indicate that a previous order with the same cart content has already paid.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const FLAG_PREVIOUS_ORDER_PAID = 'wcpay_paid_for_previous_order';
|
||||
|
||||
/**
|
||||
* Flag to indicate that a previous intention attached to the order was successful.
|
||||
*/
|
||||
const FLAG_PREVIOUS_SUCCESSFUL_INTENT = 'wcpay_previous_successful_intent';
|
||||
|
||||
/**
|
||||
* WC_Payments_Order_Service instance.
|
||||
*
|
||||
* @var WC_Payments_Order_Service
|
||||
*/
|
||||
protected $order_service;
|
||||
|
||||
/**
|
||||
* Gateway instance.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
protected $gateway;
|
||||
|
||||
/**
|
||||
* Initializes all dependencies and hooks, related to the service.
|
||||
*
|
||||
* @param WC_Payment_Gateway_WCPay $gateway The main gateway.
|
||||
* @param WC_Payments_Order_Service $order_service The order service instance.
|
||||
*/
|
||||
public function init( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Order_Service $order_service ) {
|
||||
$this->gateway = $gateway;
|
||||
$this->order_service = $order_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the attached payment intent was successful for the current order.
|
||||
*
|
||||
* @param WC_Order $order Current order to check.
|
||||
*
|
||||
* @return array|void A successful response in case the attached intent was successful, null if none.
|
||||
*/
|
||||
public function check_payment_intent_attached_to_order_succeeded( WC_Order $order ) {
|
||||
$intent_id = (string) $order->get_meta( '_intent_id', true );
|
||||
if ( empty( $intent_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only care about payment intent.
|
||||
$is_payment_intent = 'pi_' === substr( $intent_id, 0, 3 );
|
||||
if ( ! $is_payment_intent ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$request = Get_Intention::create( $intent_id );
|
||||
$request->set_hook_args( $order );
|
||||
/** @var \WC_Payments_API_Abstract_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
|
||||
$intent = $request->send();
|
||||
$intent_status = $intent->get_status();
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( 'Failed to fetch attached payment intent: ' . $e );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $intent->is_authorized() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$intent_meta_order_id_raw = $intent->get_metadata()['order_id'] ?? '';
|
||||
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
|
||||
$intent_meta_order_number_raw = $intent->get_metadata()['order_number'] ?? '';
|
||||
$intent_meta_order_number = is_numeric( $intent_meta_order_number_raw ) ? intval( $intent_meta_order_number_raw ) : 0;
|
||||
$paid_on_woopay = filter_var( $intent->get_metadata()['paid_on_woopay'] ?? false, FILTER_VALIDATE_BOOLEAN );
|
||||
$is_woopay_order = $order->get_id() === $intent_meta_order_number;
|
||||
if ( ! ( $paid_on_woopay && $is_woopay_order ) && $intent_meta_order_id !== $order->get_id() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Intent_Status::SUCCEEDED === $intent_status ) {
|
||||
$this->remove_session_processing_order( $order->get_id() );
|
||||
}
|
||||
$this->order_service->update_order_status_from_intent( $order, $intent );
|
||||
|
||||
$return_url = $this->gateway->get_return_url( $order );
|
||||
$return_url = add_query_arg( self::FLAG_PREVIOUS_SUCCESSFUL_INTENT, 'yes', $return_url );
|
||||
return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html#method_get_return_url is passed in.
|
||||
'result' => 'success',
|
||||
'redirect' => $return_url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current order has the same content with the session processing order, which was already paid (ex. by a webhook).
|
||||
*
|
||||
* @param WC_Order $current_order Current order in process_payment.
|
||||
*
|
||||
* @return array|void A successful response in case the session processing order was paid, null if none.
|
||||
*/
|
||||
public function check_against_session_processing_order( WC_Order $current_order ) {
|
||||
$session_order_id = $this->get_session_processing_order();
|
||||
if ( null === $session_order_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session_order = wc_get_order( $session_order_id );
|
||||
if ( ! is_a( $session_order, 'WC_Order' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $current_order->get_cart_hash() !== $session_order->get_cart_hash() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $session_order->has_status( wc_get_is_paid_statuses() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $current_order->has_status( wc_get_is_pending_statuses() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $session_order->get_id() === $current_order->get_id() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $session_order->get_customer_id() !== $current_order->get_customer_id() ) {
|
||||
return;
|
||||
}
|
||||
$session_order->add_order_note(
|
||||
sprintf(
|
||||
/* translators: order ID integer number */
|
||||
__( 'WooCommerce Payments: detected and deleted order ID %d, which has duplicate cart content with this order.', 'woocommerce-payments' ),
|
||||
$current_order->get_id()
|
||||
)
|
||||
);
|
||||
$current_order->delete();
|
||||
|
||||
$this->remove_session_processing_order( $session_order_id );
|
||||
|
||||
$return_url = $this->gateway->get_return_url( $session_order );
|
||||
$return_url = add_query_arg( self::FLAG_PREVIOUS_ORDER_PAID, 'yes', $return_url );
|
||||
|
||||
return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html#method_get_return_url is passed in.
|
||||
'result' => 'success',
|
||||
'redirect' => $return_url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the processing order ID for the current session.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_update_session_processing_order( int $order_id ) {
|
||||
if ( WC()->session ) {
|
||||
WC()->session->set( self::SESSION_KEY_PROCESSING_ORDER, $order_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the provided order ID from the current session if it matches with the ID in the session.
|
||||
*
|
||||
* @param int $order_id Order ID to remove from the session.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function remove_session_processing_order( int $order_id ) {
|
||||
$current_session_id = $this->get_session_processing_order();
|
||||
if ( $order_id === $current_session_id && WC()->session ) {
|
||||
WC()->session->set( self::SESSION_KEY_PROCESSING_ORDER, null );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the processing order ID for the current session.
|
||||
*
|
||||
* @return integer|null Order ID. Null if the value is not set.
|
||||
*/
|
||||
protected function get_session_processing_order() {
|
||||
$session = WC()->session;
|
||||
if ( null === $session ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$val = $session->get( self::SESSION_KEY_PROCESSING_ORDER );
|
||||
return null === $val ? null : absint( $val );
|
||||
}
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Duplicates_Detection_Service
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WC_Payments;
|
||||
use WCPay\Payment_Methods\Affirm_Payment_Method;
|
||||
use WCPay\Payment_Methods\Afterpay_Payment_Method;
|
||||
use WCPay\Payment_Methods\Bancontact_Payment_Method;
|
||||
use WCPay\Payment_Methods\Becs_Payment_Method;
|
||||
use WCPay\Payment_Methods\CC_Payment_Method;
|
||||
use WCPay\Payment_Methods\Eps_Payment_Method;
|
||||
use WCPay\Payment_Methods\Ideal_Payment_Method;
|
||||
use WCPay\Payment_Methods\Klarna_Payment_Method;
|
||||
use WCPay\Payment_Methods\P24_Payment_Method;
|
||||
use WCPay\Payment_Methods\Sepa_Payment_Method;
|
||||
|
||||
/**
|
||||
* Class handling detection of payment methods enabled by multiple plugins simultaneously.
|
||||
*/
|
||||
class Duplicates_Detection_Service {
|
||||
|
||||
/**
|
||||
* Registered gateways.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $registered_gateways = null;
|
||||
|
||||
/**
|
||||
* Gateways qualified by duplicates detector.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $gateways_qualified_by_duplicates_detector = [];
|
||||
|
||||
/**
|
||||
* Find duplicates.
|
||||
*
|
||||
* @return array Duplicated gateways.
|
||||
*/
|
||||
public function find_duplicates() {
|
||||
try {
|
||||
$this->gateways_qualified_by_duplicates_detector = [];
|
||||
|
||||
$this->search_for_cc()
|
||||
->search_for_additional_payment_methods()
|
||||
->search_for_payment_request_buttons()
|
||||
->keep_gateways_enabled_in_woopayments()
|
||||
->keep_duplicates_only();
|
||||
|
||||
// Return payment method IDs list so that front-end can successfully compare with its own list.
|
||||
return $this->gateways_qualified_by_duplicates_detector;
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::warning( 'Duplicates detection service failed silently with the following error: ' . $e->getMessage() );
|
||||
|
||||
// Fail silently and return an empty array in case of any exception.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for credit card gateways.
|
||||
*
|
||||
* @return Duplicates_Detection_Service
|
||||
*/
|
||||
private function search_for_cc() {
|
||||
$keywords = [ 'credit_card', 'creditcard', 'cc', 'card' ];
|
||||
$special_keywords = [ 'woocommerce_payments', 'stripe' ];
|
||||
|
||||
foreach ( $this->get_enabled_gateways() as $gateway ) {
|
||||
if ( $this->gateway_contains_keyword( $gateway->id, $keywords ) || in_array( $gateway->id, $special_keywords, true ) ) {
|
||||
$this->gateways_qualified_by_duplicates_detector[ CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID ][] = $gateway->id;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for additional payment methods.
|
||||
*
|
||||
* @return Duplicates_Detection_Service
|
||||
*/
|
||||
private function search_for_additional_payment_methods() {
|
||||
$keywords = [
|
||||
'bancontact' => Bancontact_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'sepa' => Sepa_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'p24' => P24_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'przelewy24' => P24_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'ideal' => Ideal_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'becs' => Becs_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'eps' => Eps_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'affirm' => Affirm_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'afterpay' => Afterpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'clearpay' => Afterpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
'klarna' => Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID,
|
||||
];
|
||||
|
||||
foreach ( $this->get_enabled_gateways() as $gateway ) {
|
||||
foreach ( $keywords as $keyword => $payment_method ) {
|
||||
if ( strpos( $gateway->id, $keyword ) !== false ) {
|
||||
$this->gateways_qualified_by_duplicates_detector[ $payment_method ][] = $gateway->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for payment request buttons.
|
||||
*
|
||||
* @return Duplicates_Detection_Service
|
||||
*/
|
||||
private function search_for_payment_request_buttons() {
|
||||
$prb_payment_method = 'apple_pay_google_pay';
|
||||
$keywords = [
|
||||
'apple_pay',
|
||||
'applepay',
|
||||
'google_pay',
|
||||
'googlepay',
|
||||
];
|
||||
|
||||
foreach ( $this->get_registered_gateways() as $gateway ) {
|
||||
if ( 'yes' === $gateway->enabled ) {
|
||||
foreach ( $keywords as $keyword ) {
|
||||
if ( strpos( $gateway->id, $keyword ) !== false ) {
|
||||
$this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id;
|
||||
break;
|
||||
} elseif ( 'yes' === $gateway->get_option( 'payment_request' ) ) {
|
||||
$this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id;
|
||||
break;
|
||||
} elseif ( 'yes' === $gateway->get_option( 'express_checkout_enabled' ) ) {
|
||||
$this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only WooCommerce Payments enabled gateways.
|
||||
*
|
||||
* @return Duplicates_Detection_Service
|
||||
*/
|
||||
private function keep_gateways_enabled_in_woopayments() {
|
||||
$woopayments_gateway_ids = array_map(
|
||||
function ( $gateway ) {
|
||||
return $gateway->id; },
|
||||
array_values( WC_Payments::get_payment_gateway_map() )
|
||||
);
|
||||
|
||||
foreach ( $this->gateways_qualified_by_duplicates_detector as $gateway_id => $gateway_ids ) {
|
||||
if ( empty( array_intersect( $gateway_ids, $woopayments_gateway_ids ) ) ) {
|
||||
unset( $this->gateways_qualified_by_duplicates_detector[ $gateway_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter payment methods found to keep duplicates only.
|
||||
*
|
||||
* @return Duplicates_Detection_Service
|
||||
*/
|
||||
private function keep_duplicates_only() {
|
||||
foreach ( $this->gateways_qualified_by_duplicates_detector as $gateway_id => $gateway_ids ) {
|
||||
if ( count( $gateway_ids ) < 2 ) {
|
||||
unset( $this->gateways_qualified_by_duplicates_detector[ $gateway_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter enabled gateways only.
|
||||
*
|
||||
* @return array Enabled gateways only.
|
||||
*/
|
||||
private function get_enabled_gateways() {
|
||||
return array_filter(
|
||||
$this->get_registered_gateways(),
|
||||
function ( $gateway ) {
|
||||
return 'yes' === $gateway->enabled;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if gateway ID contains any of the keywords.
|
||||
*
|
||||
* @param string $gateway_id Gateway ID.
|
||||
* @param array $keywords Keywords to search for.
|
||||
*
|
||||
* @return bool True if gateway ID contains any of the keywords, false otherwise.
|
||||
*/
|
||||
private function gateway_contains_keyword( $gateway_id, $keywords ) {
|
||||
foreach ( $keywords as $keyword ) {
|
||||
if ( strpos( $gateway_id, $keyword ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load registered gateways.
|
||||
*
|
||||
* @return array Registered gateways.
|
||||
*/
|
||||
private function get_registered_gateways() {
|
||||
if ( null === $this->registered_gateways ) {
|
||||
$this->registered_gateways = WC()->payment_gateways->payment_gateways();
|
||||
}
|
||||
return $this->registered_gateways;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* A class that interacts with Explat A/B tests.
|
||||
*
|
||||
* This class is experimental. It is a fork of the jetpack-abtest package and
|
||||
* updated for use with ExPlat. These changes are planned to be contributed
|
||||
* back to the upstream Jetpack package. If accepted, this class should then
|
||||
* be superseded by the Jetpack class using Composer.
|
||||
*
|
||||
* This class should not be used externally.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
* @link https://packagist.org/packages/automattic/jetpack-abtest
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
/**
|
||||
* This class provides an interface to the Explat A/B tests.
|
||||
*
|
||||
* @internal This class is experimental and should not be used externally due to planned breaking changes.
|
||||
*/
|
||||
final class Experimental_Abtest {
|
||||
/**
|
||||
* A variable to hold the tests we fetched, and their variations for the current user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $tests = [];
|
||||
|
||||
/**
|
||||
* ExPlat Anonymous ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $anon_id = null;
|
||||
|
||||
/**
|
||||
* ExPlat Platform name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $platform = 'woocommerce';
|
||||
|
||||
/**
|
||||
* Whether trcking consent is given.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $consent = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $anon_id ExPlat anonymous ID.
|
||||
* @param string $platform ExPlat platform name.
|
||||
* @param bool $consent Whether tracking consent is given.
|
||||
*/
|
||||
public function __construct( string $anon_id, string $platform, bool $consent ) {
|
||||
$this->anon_id = $anon_id;
|
||||
$this->platform = $platform;
|
||||
$this->consent = $consent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the test variation for a provided A/B test.
|
||||
*
|
||||
* @param string $test_name Name of the A/B test.
|
||||
* @return mixed A/B test variation, or null on failure.
|
||||
*/
|
||||
public function get_variation( $test_name ) {
|
||||
// Default to the control variation when users haven't consented to tracking.
|
||||
if ( ! $this->consent ) {
|
||||
return 'control';
|
||||
}
|
||||
|
||||
$variation = $this->fetch_variation( $test_name );
|
||||
|
||||
// If there was an error retrieving a variation, conceal the error for the consumer.
|
||||
if ( is_wp_error( $variation ) ) {
|
||||
return 'control';
|
||||
}
|
||||
|
||||
return $variation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache the test variation for a provided A/B test from WP.com.
|
||||
*
|
||||
* ExPlat returns a null value when the assigned variation is control or
|
||||
* an assignment has not been set. In these instances, this method returns
|
||||
* a value of "control".
|
||||
*
|
||||
* @param string $test_name Name of the A/B test.
|
||||
* @return string|array|\WP_Error A/B test variation, or error on failure.
|
||||
*/
|
||||
protected function fetch_variation( $test_name ) {
|
||||
// Make sure test name exists.
|
||||
if ( ! $test_name ) {
|
||||
return new \WP_Error( 'test_name_not_provided', 'A/B test name has not been provided.' );
|
||||
}
|
||||
|
||||
// Make sure test name is a valid one.
|
||||
if ( ! preg_match( '/^[[:alnum:]_]+$/', $test_name ) ) {
|
||||
return new \WP_Error( 'invalid_test_name', 'Invalid A/B test name.' );
|
||||
}
|
||||
|
||||
// Return internal-cached test variations.
|
||||
if ( isset( $this->tests[ $test_name ] ) ) {
|
||||
return $this->tests[ $test_name ];
|
||||
}
|
||||
|
||||
// Return external-cached test variations.
|
||||
if ( ! empty( get_transient( 'abtest_variation_' . $test_name ) ) ) {
|
||||
return get_transient( 'abtest_variation_' . $test_name );
|
||||
}
|
||||
|
||||
// Make the request to the WP.com API.
|
||||
$response = $this->request_variation( $test_name );
|
||||
|
||||
// Bail if there was an error or malformed response.
|
||||
if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
|
||||
return new \WP_Error( 'failed_to_fetch_data', 'Unable to fetch the requested data.' );
|
||||
}
|
||||
|
||||
// Decode the results.
|
||||
$results = json_decode( $response['body'], true );
|
||||
|
||||
// Bail if there were no results or there is no test variation returned.
|
||||
if ( ! is_array( $results ) || empty( $results['variations'] ) ) {
|
||||
return new \WP_Error( 'unexpected_data_format', 'Data was not returned in the expected format.' );
|
||||
}
|
||||
|
||||
// Store the variation in our internal cache.
|
||||
$this->tests[ $test_name ] = $results['variations'][ $test_name ];
|
||||
|
||||
$variation = $results['variations'][ $test_name ] ?? 'control';
|
||||
|
||||
// Store the variation in our external cache.
|
||||
if ( ! empty( $results['ttl'] ) ) {
|
||||
set_transient( 'abtest_variation_' . $test_name, $variation, $results['ttl'] );
|
||||
}
|
||||
|
||||
return $variation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the request for a variation of a provided A/B test from WP.com.
|
||||
*
|
||||
* @param string $test_name Name of the A/B test.
|
||||
* @return array|\WP_Error A/B test variation error on failure.
|
||||
*/
|
||||
protected function request_variation( $test_name ) {
|
||||
$args = [
|
||||
'experiment_name' => $test_name,
|
||||
'anon_id' => $this->anon_id,
|
||||
'woo_country_code' => get_option( 'woocommerce_default_country' ),
|
||||
];
|
||||
|
||||
$url = add_query_arg(
|
||||
$args,
|
||||
sprintf( // nosemgrep: audit.php.wp.security.xss.query-arg -- constant value is passed in to add_query_arg.
|
||||
'https://public-api.wordpress.com/wpcom/v2/experiments/0.1.0/assignments/%s',
|
||||
$this->platform
|
||||
)
|
||||
);
|
||||
|
||||
$get = wp_remote_get( $url );
|
||||
|
||||
return $get;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Class LoggerContext
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use WCPay\Internal\LoggerContext as InternalLoggerContext;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* A wrapper class for accessing LoggerContext as a singletone.
|
||||
*/
|
||||
class Logger_Context {
|
||||
/**
|
||||
* Sets a context value.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param string|int|float|bool|null $value The value to set. Null removes value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_value( $key, $value ) {
|
||||
wcpay_get_container()->get( InternalLoggerContext::class )->set_value( $key, $value );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Logger
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use WCPay\Internal\Logger as InternalLogger;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* A wrapper class for interacting with WC_Logger.
|
||||
*/
|
||||
class Logger {
|
||||
|
||||
/**
|
||||
* Add a log entry.
|
||||
*
|
||||
* Note that this depends on WC_Payments gateway property to be initialized as
|
||||
* we need this to access the plugins debug setting to figure out if the setting
|
||||
* is turned on.
|
||||
*
|
||||
* @param string $message Log message.
|
||||
*
|
||||
* @param string $level One of the following:
|
||||
* 'emergency': System is unusable.
|
||||
* 'alert': Action must be taken immediately.
|
||||
* 'critical': Critical conditions.
|
||||
* 'error': Error conditions.
|
||||
* 'warning': Warning conditions.
|
||||
* 'notice': Normal but significant condition.
|
||||
* 'info': Informational messages.
|
||||
* 'debug': Debug-level messages.
|
||||
* @param array<string, string> $context Context data.
|
||||
*/
|
||||
public static function log( $message, $level = 'info', $context = [] ) {
|
||||
wcpay_get_container()->get( InternalLogger::class )->log( $message, $level, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the gateway setting logging toggle is enabled.
|
||||
*
|
||||
* @return bool Depending on the enable_logging setting.
|
||||
*/
|
||||
public static function can_log() {
|
||||
return wcpay_get_container()->get( InternalLogger::class )->can_log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type emergency
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function emergency( $message ) {
|
||||
self::log( $message, 'emergency' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type alert
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function alert( $message ) {
|
||||
self::log( $message, 'alert' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type critical
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function critical( $message ) {
|
||||
self::log( $message, 'critical' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type error
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function error( $message ) {
|
||||
self::log( $message, 'error' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type warning
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function warning( $message ) {
|
||||
self::log( $message, 'warning' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type notice
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function notice( $message ) {
|
||||
self::log( $message, 'notice' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type info
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function info( $message ) {
|
||||
self::log( $message, 'info' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log entry of type debug
|
||||
*
|
||||
* @param string $message To send to the log file.
|
||||
*/
|
||||
public static function debug( $message ) {
|
||||
self::log( $message, 'debug' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an object for logging.
|
||||
*
|
||||
* @param string $label Label for the object.
|
||||
* @param mixed $object Object to format.
|
||||
* @return string
|
||||
*/
|
||||
public static function format_object( $label, $object ) {
|
||||
try {
|
||||
$encoded = wp_json_encode( $object, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR );
|
||||
} catch ( \JsonException $e ) {
|
||||
return sprintf( 'Error encoding object "%s": %s', $label, $e->getMessage() );
|
||||
}
|
||||
return sprintf( '%s (JSON): %s', $label, $encoded );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Payment_Information
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WCPay\Constants\Payment_Type;
|
||||
use WCPay\Constants\Payment_Initiated_By;
|
||||
use WCPay\Constants\Payment_Capture_Type;
|
||||
use WCPay\Exceptions\Invalid_Payment_Method_Exception;
|
||||
use WCPay\Payment_Methods\CC_Payment_Gateway;
|
||||
|
||||
/**
|
||||
* Mostly a wrapper containing information on a single payment.
|
||||
*/
|
||||
class Payment_Information {
|
||||
/**
|
||||
* Key used to indicate that an error occurred during the payment method creation in the client.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const PAYMENT_METHOD_ERROR = 'woocommerce_payments_payment_method_error';
|
||||
|
||||
/**
|
||||
* The ID of the payment method used for this payment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $payment_method;
|
||||
|
||||
/**
|
||||
* The order object.
|
||||
*
|
||||
* @var ?\WC_Order
|
||||
*/
|
||||
private $order;
|
||||
|
||||
/**
|
||||
* The payment token used for this payment.
|
||||
*
|
||||
* @var ?\WC_Payment_Token
|
||||
*/
|
||||
private $token;
|
||||
|
||||
/**
|
||||
* The CVC confirmation used for this payment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $cvc_confirmation;
|
||||
|
||||
/**
|
||||
* Indicates whether the payment is merchant-initiated (true) or customer-initiated (false).
|
||||
*
|
||||
* @var ?Payment_Initiated_By
|
||||
*/
|
||||
private $payment_initiated_by;
|
||||
|
||||
/**
|
||||
* Indicates whether the payment will be only authorized (true) or captured immediately (false).
|
||||
*
|
||||
* @var ?Payment_Capture_Type
|
||||
*/
|
||||
private $manual_capture;
|
||||
|
||||
/**
|
||||
* The type of the payment. `single`, `recurring`, etc.
|
||||
*
|
||||
* @var Payment_Type
|
||||
*/
|
||||
private $payment_type;
|
||||
|
||||
/**
|
||||
* Indicates whether the payment method should be saved to the store.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $save_payment_method_to_store = false;
|
||||
|
||||
/**
|
||||
* Indicates whether the payment method should be saved to the platform.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $save_payment_method_to_platform = false;
|
||||
|
||||
/**
|
||||
* Indicates whether user is changing payment method for subscriptions order.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_changing_payment_method_for_subscription = false;
|
||||
|
||||
/**
|
||||
* The attached fingerprint.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $fingerprint = '';
|
||||
|
||||
/**
|
||||
* The Stripe ID of the payment method used for this payment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $payment_method_stripe_id;
|
||||
|
||||
/**
|
||||
* The WCPay Customer ID that owns the payment token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $customer_id;
|
||||
|
||||
/**
|
||||
* Will be set if there was an error during setup.
|
||||
*
|
||||
* @var ?\WP_Error
|
||||
*/
|
||||
private $error = null;
|
||||
|
||||
/**
|
||||
* Payment information constructor.
|
||||
*
|
||||
* @param string $payment_method The ID of the payment method used for this payment.
|
||||
* @param \WC_Order $order The order object.
|
||||
* @param Payment_Type $payment_type The type of the payment.
|
||||
* @param \WC_Payment_Token $token The payment token used for this payment.
|
||||
* @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated.
|
||||
* @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately.
|
||||
* @param string $cvc_confirmation The CVC confirmation for this payment method.
|
||||
* @param string $fingerprint The attached fingerprint.
|
||||
* @param string $payment_method_stripe_id The Stripe ID of the payment method used for this payment.
|
||||
* @param string $customer_id The WCPay Customer ID that owns the payment token.
|
||||
*
|
||||
* @throws Invalid_Payment_Method_Exception When no payment method is found in the provided request.
|
||||
*/
|
||||
public function __construct(
|
||||
string $payment_method,
|
||||
?\WC_Order $order = null,
|
||||
?Payment_Type $payment_type = null,
|
||||
?\WC_Payment_Token $token = null,
|
||||
?Payment_Initiated_By $payment_initiated_by = null,
|
||||
?Payment_Capture_Type $manual_capture = null,
|
||||
?string $cvc_confirmation = null,
|
||||
string $fingerprint = '',
|
||||
?string $payment_method_stripe_id = null,
|
||||
?string $customer_id = null
|
||||
) {
|
||||
if ( empty( $payment_method ) && empty( $token ) && ! \WC_Payments::is_network_saved_cards_enabled() ) {
|
||||
// If network-wide cards are enabled, a payment method or token may not be specified and the platform default one will be used.
|
||||
throw new Invalid_Payment_Method_Exception(
|
||||
esc_html__( 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.', 'woocommerce-payments' ),
|
||||
'payment_method_not_provided'
|
||||
);
|
||||
}
|
||||
$this->payment_method = $payment_method;
|
||||
$this->order = $order;
|
||||
$this->token = $token;
|
||||
$this->payment_initiated_by = $payment_initiated_by ?? Payment_Initiated_By::CUSTOMER();
|
||||
$this->manual_capture = $manual_capture ?? Payment_Capture_Type::AUTOMATIC();
|
||||
$this->payment_type = $payment_type ?? Payment_Type::SINGLE();
|
||||
$this->cvc_confirmation = $cvc_confirmation;
|
||||
$this->fingerprint = $fingerprint;
|
||||
$this->payment_method_stripe_id = $payment_method_stripe_id;
|
||||
$this->customer_id = $customer_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if payment was initiated by the merchant, false otherwise.
|
||||
*
|
||||
* @return bool True if payment was initiated by the merchant, false otherwise.
|
||||
*/
|
||||
public function is_merchant_initiated(): bool {
|
||||
return $this->payment_initiated_by->equals( Payment_Initiated_By::MERCHANT() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payment method ID.
|
||||
*
|
||||
* @return string The payment method ID.
|
||||
*/
|
||||
public function get_payment_method(): string {
|
||||
// Use the token if we have it.
|
||||
if ( $this->is_using_saved_payment_method() ) {
|
||||
return $this->token->get_token();
|
||||
}
|
||||
|
||||
return $this->payment_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the order object.
|
||||
*
|
||||
* @return ?\WC_Order The order object.
|
||||
*/
|
||||
public function get_order(): ?\WC_Order {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payment token.
|
||||
*
|
||||
* TODO: Once php requirement is bumped to >= 7.1.0 change return type to ?\WC_Payment_Token
|
||||
* since the return type is nullable, as per
|
||||
* https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
|
||||
*
|
||||
* @return ?\WC_Payment_Token The payment token.
|
||||
*/
|
||||
public function get_payment_token(): ?\WC_Payment_Token {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the payment token associated with this payment.
|
||||
*
|
||||
* @param \WC_Payment_Token $token The new payment token.
|
||||
*/
|
||||
public function set_token( \WC_Payment_Token $token ) {
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the payment token is not empty, false otherwise.
|
||||
*
|
||||
* @return bool True if payment token is not empty, false otherwise.
|
||||
*/
|
||||
public function is_using_saved_payment_method(): bool {
|
||||
return ! empty( $this->token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the payment should be only authorized, false if it should be captured immediately.
|
||||
*
|
||||
* @return bool True if the payment should be only authorized, false if it should be captured immediately.
|
||||
*/
|
||||
public function is_using_manual_capture(): bool {
|
||||
return $this->manual_capture->equals( Payment_Capture_Type::MANUAL() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment information constructor.
|
||||
*
|
||||
* @param array $request Associative array containing payment request information.
|
||||
* @param \WC_Order $order The order object.
|
||||
* @param Payment_Type $payment_type The type of the payment.
|
||||
* @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated.
|
||||
* @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately.
|
||||
* @param string $payment_method_stripe_id The Stripe ID of the payment method used for this payment.
|
||||
*
|
||||
* @throws \Exception - If no payment method is found in the provided request.
|
||||
*/
|
||||
public static function from_payment_request(
|
||||
array $request,
|
||||
?\WC_Order $order = null,
|
||||
?Payment_Type $payment_type = null,
|
||||
?Payment_Initiated_By $payment_initiated_by = null,
|
||||
?Payment_Capture_Type $manual_capture = null,
|
||||
?string $payment_method_stripe_id = null
|
||||
): Payment_Information {
|
||||
$payment_method = self::get_payment_method_from_request( $request );
|
||||
$token = self::get_token_from_request( $request );
|
||||
$cvc_confirmation = self::get_cvc_confirmation_from_request( $request );
|
||||
$fingerprint = self::get_fingerprint_from_request( $request );
|
||||
|
||||
if ( isset( $request['is_woopay'] ) && $request['is_woopay'] ) {
|
||||
$order->add_meta_data( 'is_woopay', true, true );
|
||||
$order->save_meta_data();
|
||||
}
|
||||
$payment_information = new Payment_Information( $payment_method, $order, $payment_type, $token, $payment_initiated_by, $manual_capture, $cvc_confirmation, $fingerprint, $payment_method_stripe_id );
|
||||
|
||||
if ( self::PAYMENT_METHOD_ERROR === $payment_method ) {
|
||||
$error_message = $request['wcpay-payment-method-error-message'] ?? __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' );
|
||||
$error_code = $request['wcpay-payment-method-error-code'] ?? 'unknown-error';
|
||||
$error = new \WP_Error( $error_code, $error_message );
|
||||
$payment_information->set_error( $error );
|
||||
}
|
||||
|
||||
return $payment_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the payment method from the provided request.
|
||||
*
|
||||
* @param array $request Associative array containing payment request information.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_payment_method_from_request( array $request ): string {
|
||||
foreach ( [ 'wcpay-payment-method', 'wcpay-payment-method-sepa' ] as $key ) {
|
||||
if ( ! empty( $request[ $key ] ) ) {
|
||||
$normalized = wc_clean( $request[ $key ] );
|
||||
return is_string( $normalized ) ? $normalized : '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the payment token from the provided request.
|
||||
*
|
||||
* TODO: Once php requirement is bumped to >= 7.1.0 set return type to ?\WC_Payment_Token
|
||||
* since the return type is nullable, as per
|
||||
* https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
|
||||
*
|
||||
* @param array $request Associative array containing payment request information.
|
||||
*
|
||||
* @return \WC_Payment_Token|NULL
|
||||
*/
|
||||
public static function get_token_from_request( array $request ) {
|
||||
$payment_method = $request['payment_method'] ?? null;
|
||||
$token_request_key = 'wc-' . $payment_method . '-payment-token';
|
||||
if (
|
||||
! isset( $request[ $token_request_key ] ) ||
|
||||
'new' === $request[ $token_request_key ]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$token = \WC_Payment_Tokens::get( wc_clean( $request[ $token_request_key ] ) );
|
||||
|
||||
// If the token doesn't belong to this gateway or the current user it's invalid.
|
||||
if ( ! $token || $payment_method !== $token->get_gateway_id() || $token->get_user_id() !== get_current_user_id() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the payment CVC confirmation from the provided request.
|
||||
*
|
||||
* @param array $request Associative array containing payment request information.
|
||||
*
|
||||
* @return string|NULL
|
||||
*/
|
||||
public static function get_cvc_confirmation_from_request( array $request ) {
|
||||
$payment_method = $request['payment_method'] ?? null;
|
||||
if ( null === $payment_method ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cvc_request_key = 'wc-' . $payment_method . '-payment-cvc-confirmation';
|
||||
if (
|
||||
! isset( $request[ $cvc_request_key ] ) ||
|
||||
'new' === $request[ $cvc_request_key ]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $request[ $cvc_request_key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the fingerprint data from the provided request.
|
||||
*
|
||||
* @param array $request Associative array containing payment request information.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_fingerprint_from_request( array $request ) {
|
||||
if ( ! empty( $request['wcpay-fingerprint'] ) ) {
|
||||
$normalized = wc_clean( $request['wcpay-fingerprint'] );
|
||||
return is_string( $normalized ) ? $normalized : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the type of the payment.
|
||||
*
|
||||
* @param Payment_Type $type The new type.
|
||||
*/
|
||||
public function set_payment_type( $type ) {
|
||||
$this->payment_type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the type of the payment.
|
||||
*
|
||||
* @return Payment_Type The payment type.
|
||||
*/
|
||||
public function get_payment_type() {
|
||||
return $this->payment_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the payment method to be saved to merchant store when the payment gets processed.
|
||||
*/
|
||||
public function must_save_payment_method_to_store() {
|
||||
$this->save_payment_method_to_store = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the payment method to be saved to platform when the payment gets processed.
|
||||
*/
|
||||
public function must_save_payment_method_to_platform() {
|
||||
$this->save_payment_method_to_platform = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the payment method needs to be saved to the store for later usage.
|
||||
*
|
||||
* @return bool The flag.
|
||||
*/
|
||||
public function should_save_payment_method_to_store() {
|
||||
return ! $this->is_using_saved_payment_method() && $this->save_payment_method_to_store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the payment method needs to be saved to the platform for later usage.
|
||||
*
|
||||
* @return bool The flag.
|
||||
*/
|
||||
public function should_save_payment_method_to_platform() {
|
||||
return ! $this->is_using_saved_payment_method() && $this->save_payment_method_to_platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that we are changing payment for subscriptions order or not.
|
||||
*
|
||||
* @param bool $is_changing_payment_method_for_subscription Whether or not we are changing payment for subscriptions order.
|
||||
*/
|
||||
public function set_is_changing_payment_method_for_subscription( bool $is_changing_payment_method_for_subscription ) {
|
||||
$this->is_changing_payment_method_for_subscription = $is_changing_payment_method_for_subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flag of whether or not we are changing payment for subscriptions order.
|
||||
*
|
||||
* @return bool Whether or not we are changing payment for subscriptions order.
|
||||
*/
|
||||
public function is_changing_payment_method_for_subscription(): bool {
|
||||
return $this->is_changing_payment_method_for_subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payment method CVC confirmation.
|
||||
*
|
||||
* @return string|NULL The payment method CVC confirmation.
|
||||
*/
|
||||
public function get_cvc_confirmation() {
|
||||
return $this->cvc_confirmation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attached fingerprint.
|
||||
*
|
||||
* @return string The attached fingerprint.
|
||||
*/
|
||||
public function get_fingerprint() {
|
||||
return $this->fingerprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Stripe ID of payment method.
|
||||
*
|
||||
* @return string The Stripe ID of payment method.
|
||||
*/
|
||||
public function get_payment_method_stripe_id() {
|
||||
return $this->payment_method_stripe_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WCPay Customer ID that owns the payment token.
|
||||
*
|
||||
* @return string The WCPay Customer ID.
|
||||
*/
|
||||
public function get_customer_id() {
|
||||
return $this->customer_id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the error data.
|
||||
*
|
||||
* @param \WP_Error $error The error to be set.
|
||||
* @return void
|
||||
*/
|
||||
public function set_error( \WP_Error $error ) {
|
||||
$this->error = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error data.
|
||||
*
|
||||
* @return ?\WP_Error
|
||||
*/
|
||||
public function get_error() {
|
||||
return $this->error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Session_Rate_Limiter
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* A wrapper class for keeping track of events in registries, and to trigger a rate limiter after a threshold.
|
||||
*/
|
||||
class Session_Rate_Limiter {
|
||||
|
||||
/**
|
||||
* Key used in the session to store card declined transactions.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const SESSION_KEY_DECLINED_CARD_REGISTRY = 'wcpay_card_declined_registry';
|
||||
|
||||
/**
|
||||
* Key used to store the registry in the session
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* Number of elements in the registry needed to enable the rate limiter
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $threshold;
|
||||
|
||||
/**
|
||||
* Number of seconds the limiter is enabled for after the threshold is reached
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $delay;
|
||||
|
||||
/**
|
||||
* Session_Rate_Limiter constructor.
|
||||
*
|
||||
* @param string $key - Key for the registry.
|
||||
* @param int $threshold - Number of elements in the registry before enabling the limiter.
|
||||
* @param int $delay - Number of seconds the limiter will be in use after threshold is reached.
|
||||
*/
|
||||
public function __construct(
|
||||
$key,
|
||||
$threshold,
|
||||
$delay
|
||||
) {
|
||||
$this->key = $key;
|
||||
$this->threshold = $threshold;
|
||||
$this->delay = $delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an event in an specified registry using a key.
|
||||
* If the number of events in the registry match the threshold,
|
||||
* a new rate limiter is enabled with the given delay.
|
||||
*
|
||||
* The registry of declined card attemps is cleaned after a new rate limiter is enabled.
|
||||
*/
|
||||
public function bump() {
|
||||
if ( ! isset( WC()->session ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$registry = WC()->session->get( $this->key ) ?? [];
|
||||
$registry[] = time();
|
||||
WC()->session->set( $this->key, $registry );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the rate limiter is enabled.
|
||||
*
|
||||
* Returns a boolean.
|
||||
*
|
||||
* @return bool The rate limiter is in use.
|
||||
*/
|
||||
public function is_limited(): bool {
|
||||
if ( ! isset( WC()->session ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( 'yes' === get_option( 'wcpay_session_rate_limiter_disabled_' . $this->key ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$registry = WC()->session->get( $this->key ) ?? [];
|
||||
|
||||
if ( ( is_countable( $registry ) ? count( $registry ) : 0 ) >= $this->threshold ) {
|
||||
$start_time_limiter = end( $registry );
|
||||
$next_try_allowed_at = $start_time_limiter + $this->delay;
|
||||
$is_limited = time() <= $next_try_allowed_at;
|
||||
if ( ! $is_limited ) {
|
||||
WC()->session->set( $this->key, [] );
|
||||
}
|
||||
|
||||
return $is_limited;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+122
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payment_Token_WCPay_Link
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Stripe Link Payment Token.
|
||||
*
|
||||
* Representation of a payment token for Link.
|
||||
*
|
||||
* @class WC_Payment_Token_WCPay_Link
|
||||
*/
|
||||
class WC_Payment_Token_WCPay_Link extends WC_Payment_Token {
|
||||
|
||||
/**
|
||||
* Class Constant so other code can be unambiguous.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const TYPE = 'wcpay_link';
|
||||
|
||||
/**
|
||||
* The payment method type of this token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = self::TYPE;
|
||||
|
||||
/**
|
||||
* Stores Link payment token data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extra_data = [
|
||||
'email' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get payment method type to display to user.
|
||||
*
|
||||
* @param string $deprecated Deprecated since WooCommerce 3.0.
|
||||
* @return string
|
||||
*/
|
||||
public function get_display_name( $deprecated = '' ) {
|
||||
$display = sprintf(
|
||||
/* translators: customer email */
|
||||
__( 'Stripe Link email ending in %s', 'woocommerce-payments' ),
|
||||
$this->get_redacted_email()
|
||||
);
|
||||
|
||||
return $display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook prefix.
|
||||
*/
|
||||
protected function get_hook_prefix() {
|
||||
return 'woocommerce_payments_token_wcpay_link_get_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the customer email.
|
||||
*
|
||||
* @param string $context What the value is for. Valid values are view and edit.
|
||||
*
|
||||
* @return string Customer email.
|
||||
*/
|
||||
public function get_email( $context = 'view' ) {
|
||||
return $this->get_prop( 'email', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the customer email.
|
||||
*
|
||||
* @param string $email Customer email.
|
||||
*/
|
||||
public function set_email( $email ) {
|
||||
$this->set_prop( 'email', $email );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns redacted/shortened customer email
|
||||
*
|
||||
* @param string $context What the value is for. Valid values are view and edit.
|
||||
* @return string Redacted/shortened customer email.
|
||||
*/
|
||||
public function get_redacted_email( $context = 'view' ) {
|
||||
return $this->redact_email_address( $this->get_email( $context ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of this payment token (CC, eCheck, or something else).
|
||||
*
|
||||
* @param string $deprecated Deprecated since WooCommerce 3.0.
|
||||
* @return string Payment Token Type (CC, eCheck)
|
||||
*/
|
||||
public function get_type( $deprecated = '' ) {
|
||||
return self::TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms email address into redacted/shortened format like ***xxxx@x.com.
|
||||
* Using shortened length of four characters will mimic CC last-4 digits of card number.
|
||||
*
|
||||
* @param string $email Email address.
|
||||
* @return string Redacted/shortened email address.
|
||||
*/
|
||||
private function redact_email_address( $email ) {
|
||||
$placeholder = '***';
|
||||
$shortened_length = 4;
|
||||
list( $handle, $domain ) = explode( '@', $email );
|
||||
$redacted_handle = strlen( $handle ) > $shortened_length ? substr( $handle, - $shortened_length ) : $handle;
|
||||
|
||||
return "$placeholder$redacted_handle@$domain";
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payment_Token_WCPay_SEPA
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Stripe SEPA Direct Debit Payment Token.
|
||||
*
|
||||
* Representation of a payment token for SEPA.
|
||||
*
|
||||
* @class WC_Payment_Token_WCPay_SEPA
|
||||
*/
|
||||
class WC_Payment_Token_WCPay_SEPA extends WC_Payment_Token {
|
||||
|
||||
/**
|
||||
* Class Constant so other code can be unambiguous.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const TYPE = 'wcpay_sepa';
|
||||
|
||||
/**
|
||||
* The payment method type of this token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = self::TYPE;
|
||||
|
||||
/**
|
||||
* Stores SEPA payment token data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extra_data = [
|
||||
'last4' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get type to display to user.
|
||||
*
|
||||
* @param string $deprecated Deprecated since WooCommerce 3.0.
|
||||
* @return string
|
||||
*/
|
||||
public function get_display_name( $deprecated = '' ) {
|
||||
$display = sprintf(
|
||||
/* translators: last 4 digits of IBAN account */
|
||||
__( 'SEPA IBAN ending in %s', 'woocommerce-payments' ),
|
||||
$this->get_last4()
|
||||
);
|
||||
|
||||
return $display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook prefix.
|
||||
*/
|
||||
protected function get_hook_prefix() {
|
||||
return 'woocommerce_payments_token_wcpay_sepa_get_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SEPA payment tokens.
|
||||
*
|
||||
* These fields are required by all SEPA payment tokens:
|
||||
* last4 - string Last 4 digits of the iBAN
|
||||
*
|
||||
* @return boolean True if the passed data is valid
|
||||
*/
|
||||
public function validate() {
|
||||
if ( false === parent::validate() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->get_last4( 'edit' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last four digits.
|
||||
*
|
||||
* @param string $context What the value is for. Valid values are view and edit.
|
||||
* @return string Last 4 digits
|
||||
*/
|
||||
public function get_last4( $context = 'view' ) {
|
||||
return $this->get_prop( 'last4', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last four digits.
|
||||
*
|
||||
* @param string $last4 SEPA Debit last four digits.
|
||||
*/
|
||||
public function set_last4( $last4 ) {
|
||||
$this->set_prop( 'last4', $last4 );
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+228
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Action_Scheduler_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Compatibility_Service;
|
||||
use WCPay\Constants\Order_Mode;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class which handles setting up all ActionScheduler hooks.
|
||||
*/
|
||||
class WC_Payments_Action_Scheduler_Service {
|
||||
|
||||
const GROUP_ID = 'woocommerce_payments';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* WC_Payments_Order_Service instance for updating order statuses.
|
||||
*
|
||||
* @var WC_Payments_Order_Service
|
||||
*/
|
||||
private $order_service;
|
||||
|
||||
/**
|
||||
* Compatibility service instance for updating compatibility data.
|
||||
*
|
||||
* @var Compatibility_Service
|
||||
*/
|
||||
private $compatibility_service;
|
||||
|
||||
/**
|
||||
* Constructor for WC_Payments_Action_Scheduler_Service.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client.
|
||||
* @param WC_Payments_Order_Service $order_service - Order Service.
|
||||
* @param Compatibility_Service $compatibility_service - Compatibility service instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $payments_api_client,
|
||||
WC_Payments_Order_Service $order_service,
|
||||
Compatibility_Service $compatibility_service
|
||||
) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
$this->order_service = $order_service;
|
||||
$this->compatibility_service = $compatibility_service;
|
||||
|
||||
$this->add_action_scheduler_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach hooks for all ActionScheduler actions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_action_scheduler_hooks() {
|
||||
add_action( 'wcpay_track_new_order', [ $this, 'track_new_order_action' ] );
|
||||
add_action( 'wcpay_track_update_order', [ $this, 'track_update_order_action' ] );
|
||||
add_action( WC_Payments_Order_Service::ADD_FEE_BREAKDOWN_TO_ORDER_NOTES, [ $this->order_service, 'add_fee_breakdown_to_order_notes' ], 10, 3 );
|
||||
add_action( Compatibility_Service::UPDATE_COMPATIBILITY_DATA, [ $this->compatibility_service, 'update_compatibility_data_hook' ], 10, 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is a hook that will be called by ActionScheduler when an order is created.
|
||||
* It will make a request to the Payments API to track this event.
|
||||
*
|
||||
* @param array $order_id The ID of the order that has been created.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function track_new_order_action( $order_id ) {
|
||||
return $this->track_order( $order_id, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is a hook that will be called by ActionScheduler when an order is updated.
|
||||
* It will make a request to the Payments API to track this event.
|
||||
*
|
||||
* @param int $order_id The ID of the order which has been updated.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function track_update_order_action( $order_id ) {
|
||||
return $this->track_order( $order_id, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an order by making a request to the Payments API.
|
||||
*
|
||||
* @param mixed $order_id The ID of the order which has been updated/created.
|
||||
* @param bool $is_update Is this an update event. If false, it is assumed this is a creation event.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function track_order( $order_id, $is_update = false ) {
|
||||
// Get the order details.
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we do not have a valid payment method for this order, don't send the request.
|
||||
$payment_method = $this->order_service->get_payment_method_id_for_order( $order );
|
||||
if ( empty( $payment_method ) ) {
|
||||
return false;
|
||||
}
|
||||
$order_mode = $order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY );
|
||||
|
||||
if ( $order_mode ) {
|
||||
$current_mode = WC_Payments::mode()->is_test() ? Order_Mode::TEST : Order_Mode::PRODUCTION;
|
||||
if ( $current_mode !== $order_mode ) {
|
||||
// If mode doesn't match make sure to stop order tracking to prevent order tracking issues.
|
||||
// False will be returned so maybe future crons will have correct mode.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the order data to the Payments API to track it.
|
||||
$response = $this->payments_api_client->track_order(
|
||||
array_merge(
|
||||
$order->get_data(),
|
||||
[
|
||||
'_payment_method_id' => $payment_method,
|
||||
'_stripe_customer_id' => $this->order_service->get_customer_id_for_order( $order ),
|
||||
'_wcpay_mode' => $order_mode,
|
||||
]
|
||||
),
|
||||
$is_update
|
||||
);
|
||||
|
||||
if ( 'success' === $response['result'] && ! $is_update ) {
|
||||
// Update the metadata to reflect that the order creation event has been fired.
|
||||
$order->add_meta_data( '_new_order_tracking_complete', 'yes' );
|
||||
$order->save_meta_data();
|
||||
}
|
||||
|
||||
return ( 'success' === ( $response['result'] ?? null ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action scheduler job.
|
||||
*
|
||||
* Also, unschedules (replaces) any previous instances of the same job.
|
||||
* This prevents duplicate jobs, for example when multiple events fire as part of the order update process.
|
||||
* We will only replace a job which has the same $hook, $args AND $group.
|
||||
*
|
||||
* @param int $timestamp When the job will run.
|
||||
* @param string $hook The hook to trigger.
|
||||
* @param array $args Optional. An array containing the arguments to be passed to the hook.
|
||||
* Defaults to an empty array.
|
||||
* @param string $group Optional. The AS group the action will be created under.
|
||||
* Defaults to 'woocommerce_payments'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function schedule_job( int $timestamp, string $hook, array $args = [], string $group = self::GROUP_ID ) {
|
||||
// The `action_scheduler_init` hook was introduced in ActionScheduler 3.5.5 (WooCommerce 7.9.0).
|
||||
if ( version_compare( WC()->version, '7.9.0', '>=' ) ) {
|
||||
// If the ActionScheduler is already initialized, schedule the job.
|
||||
if ( did_action( 'action_scheduler_init' ) ) {
|
||||
$this->schedule_action_and_prevent_duplicates( $timestamp, $hook, $args, $group );
|
||||
} else {
|
||||
// The ActionScheduler is not initialized yet; we need to schedule the job when it fires the init hook.
|
||||
add_action(
|
||||
'action_scheduler_init',
|
||||
function () use ( $timestamp, $hook, $args, $group ) {
|
||||
$this->schedule_action_and_prevent_duplicates( $timestamp, $hook, $args, $group );
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->schedule_action_and_prevent_duplicates( $timestamp, $hook, $args, $group );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if there is a Pending action with the same hook already.
|
||||
*
|
||||
* @param string $hook Hook name.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function pending_action_exists( string $hook ): bool {
|
||||
$actions = as_get_scheduled_actions(
|
||||
[
|
||||
'hook' => $hook,
|
||||
'status' => ActionScheduler_Store::STATUS_PENDING,
|
||||
'group' => self::GROUP_ID,
|
||||
]
|
||||
);
|
||||
|
||||
return ( is_countable( $actions ) ? count( $actions ) : 0 ) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action while unscheduling any scheduled actions that are exactly the same.
|
||||
*
|
||||
* We will look for scheduled actions with the same name, args and group when unscheduling.
|
||||
*
|
||||
* @param int $timestamp When the action will run.
|
||||
* @param string $action The action name to schedule.
|
||||
* @param array $args Optional. An array containing the arguments to be passed to the action.
|
||||
* Defaults to an empty array.
|
||||
* @param string $group Optional. The ActionScheduler group the action will be created under.
|
||||
* Defaults to 'woocommerce_payments'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_action_and_prevent_duplicates( int $timestamp, string $action, array $args = [], string $group = self::GROUP_ID ) {
|
||||
// Unschedule any previously scheduled actions with the same name, args, and group combination.
|
||||
// It is more efficient/performant to check if the action is already scheduled before unscheduling it.
|
||||
// @see https://github.com/Automattic/woocommerce-payments/issues/6662.
|
||||
if ( as_has_scheduled_action( $action, $args, $group ) ) {
|
||||
as_unschedule_action( $action, $args, $group );
|
||||
}
|
||||
|
||||
as_schedule_single_action( $timestamp, $action, $args, $group );
|
||||
}
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Apple_Pay_Registration
|
||||
*
|
||||
* Adapted from WooCommerce Stripe Gateway extension.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use WCPay\Logger;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
use WCPay\Tracker;
|
||||
|
||||
/**
|
||||
* WC_Payments_Apple_Pay_Registration class.
|
||||
*/
|
||||
class WC_Payments_Apple_Pay_Registration {
|
||||
|
||||
const DOMAIN_ASSOCIATION_FILE_NAME = 'apple-developer-merchantid-domain-association';
|
||||
const DOMAIN_ASSOCIATION_FILE_DIR = '.well-known';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* The WCPay account object.
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC_Payment_Gateway_WCPay instance.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* Current domain name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $domain_name;
|
||||
|
||||
/**
|
||||
* Stores Apple Pay domain verification issues.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $apple_pay_verify_notice;
|
||||
|
||||
/**
|
||||
* Initialize class actions.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client.
|
||||
* @param WC_Payments_Account $account WooCommerce Payments account.
|
||||
* @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments gateway.
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway ) {
|
||||
$this->domain_name = wp_parse_url( get_site_url(), PHP_URL_HOST );
|
||||
$this->apple_pay_verify_notice = '';
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
$this->account = $account;
|
||||
$this->gateway = $gateway;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'init', [ $this, 'add_domain_association_rewrite_rule' ], 5 );
|
||||
add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'verify_domain_on_update' ] );
|
||||
add_action( 'init', [ $this, 'init' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'admin_init', [ $this, 'verify_domain_on_domain_name_change' ] );
|
||||
add_filter( 'query_vars', [ $this, 'whitelist_domain_association_query_param' ], 10, 1 );
|
||||
add_action( 'parse_request', [ $this, 'parse_domain_association_request' ], 10, 1 );
|
||||
|
||||
add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_error_notice' ] );
|
||||
add_action( 'add_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_new_settings' ], 10, 2 );
|
||||
add_action( 'update_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_updated_settings' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the gateway and Express Checkout Buttons (prerequisites for Apple Pay) are enabled.
|
||||
*
|
||||
* @return bool Whether Apple Pay required settings are enabled.
|
||||
*/
|
||||
private function is_enabled() {
|
||||
return $this->gateway->is_enabled() && 'yes' === $this->gateway->get_option( 'payment_request' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the gateway and Express Checkout Buttons were enabled in previous settings.
|
||||
*
|
||||
* @param array|null $prev_settings Gateway settings.
|
||||
*
|
||||
* @return bool Whether Apple Pay required settings are enabled.
|
||||
*/
|
||||
private function was_enabled( $prev_settings ) {
|
||||
$gateway_enabled = 'yes' === ( $prev_settings['enabled'] ?? 'no' );
|
||||
$payment_request_enabled = 'yes' === ( $prev_settings['payment_request'] ?? 'no' );
|
||||
return $gateway_enabled && $payment_request_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger Apple Pay registration upon domain name change.
|
||||
*/
|
||||
public function verify_domain_on_domain_name_change() {
|
||||
$verified_domain = $this->gateway->get_option( 'apple_pay_verified_domain' );
|
||||
if ( $this->domain_name !== $verified_domain ) {
|
||||
$this->verify_domain_if_configured();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify domain upon plugin update only in case the domain association file has changed.
|
||||
*/
|
||||
public function verify_domain_on_update() {
|
||||
if ( $this->is_enabled() && ! $this->is_hosted_domain_association_file_up_to_date() ) {
|
||||
$this->verify_domain_if_configured();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vefifies if hosted domain association file is up to date
|
||||
* with the file from the plugin directory.
|
||||
*
|
||||
* @return bool Whether file is up to date or not.
|
||||
*/
|
||||
private function is_hosted_domain_association_file_up_to_date() {
|
||||
$fullpath = untrailingslashit( ABSPATH ) . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
|
||||
if ( ! file_exists( $fullpath ) ) {
|
||||
return false;
|
||||
}
|
||||
// Contents of domain association file from plugin dir.
|
||||
$new_contents = @file_get_contents( WCPAY_ABSPATH . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME ); // @codingStandardsIgnoreLine
|
||||
// Get file contents from local path and remote URL and check if either of which matches.
|
||||
$local_contents = @file_get_contents( $fullpath ); // @codingStandardsIgnoreLine
|
||||
$url = get_site_url() . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
|
||||
$response = @wp_remote_get( $url ); // @codingStandardsIgnoreLine
|
||||
$remote_contents = @wp_remote_retrieve_body( $response ); // @codingStandardsIgnoreLine
|
||||
|
||||
return $local_contents === $new_contents || $remote_contents === $new_contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies and overwrites domain association file.
|
||||
*
|
||||
* @return null|string Error message.
|
||||
*/
|
||||
private function copy_and_overwrite_domain_association_file() {
|
||||
$well_known_dir = untrailingslashit( ABSPATH ) . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR;
|
||||
$fullpath = $well_known_dir . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
|
||||
|
||||
if ( ! is_dir( $well_known_dir ) && ! @mkdir( $well_known_dir, 0755 ) && ! is_dir( $well_known_dir ) ) { // @codingStandardsIgnoreLine
|
||||
return __( 'Unable to create domain association folder to domain root.', 'woocommerce-payments' );
|
||||
}
|
||||
|
||||
if ( ! @copy( WCPAY_ABSPATH . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME, $fullpath ) ) { // @codingStandardsIgnoreLine
|
||||
return __( 'Unable to copy domain association file to domain root.', 'woocommerce-payments' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Apple Pay domain association file.
|
||||
* Reports failure only if file isn't already being served properly.
|
||||
*/
|
||||
public function update_domain_association_file() {
|
||||
if ( $this->is_hosted_domain_association_file_up_to_date() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$error_message = $this->copy_and_overwrite_domain_association_file();
|
||||
|
||||
if ( isset( $error_message ) ) {
|
||||
$url = get_site_url() . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
|
||||
Logger::log(
|
||||
'Error: ' . $error_message . ' ' .
|
||||
/* translators: expected domain association file URL */
|
||||
sprintf( __( 'To enable Apple Pay, domain association file must be hosted at %s.', 'woocommerce-payments' ), $url )
|
||||
);
|
||||
} else {
|
||||
Logger::log( __( 'Domain association file updated.', 'woocommerce-payments' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a rewrite rule for serving the domain association file from the proper location.
|
||||
*/
|
||||
public function add_domain_association_rewrite_rule() {
|
||||
$regex = '^\\' . self::DOMAIN_ASSOCIATION_FILE_DIR . '\/' . self::DOMAIN_ASSOCIATION_FILE_NAME . '$';
|
||||
$redirect = 'index.php?' . self::DOMAIN_ASSOCIATION_FILE_NAME . '=1';
|
||||
|
||||
add_rewrite_rule( $regex, $redirect, 'top' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the list of publicly allowed query variables.
|
||||
*
|
||||
* @param array $query_vars - provided public query vars.
|
||||
* @return array Updated public query vars.
|
||||
*/
|
||||
public function whitelist_domain_association_query_param( $query_vars ) {
|
||||
$query_vars[] = self::DOMAIN_ASSOCIATION_FILE_NAME;
|
||||
return $query_vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve domain association file when proper query param is provided.
|
||||
*
|
||||
* @param object $wp WordPress environment object.
|
||||
*/
|
||||
public function parse_domain_association_request( $wp ) {
|
||||
if (
|
||||
! isset( $wp->query_vars[ self::DOMAIN_ASSOCIATION_FILE_NAME ] ) ||
|
||||
'1' !== $wp->query_vars[ self::DOMAIN_ASSOCIATION_FILE_NAME ]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = WCPAY_ABSPATH . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
|
||||
header( 'Content-Type: text/plain;charset=utf-8' );
|
||||
echo esc_html( @file_get_contents( $path ) ); // @codingStandardsIgnoreLine
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of the current mode. One of:
|
||||
* - 'dev'
|
||||
* - 'test'
|
||||
* - 'live'
|
||||
*
|
||||
* @return string A string representation of the current mode.
|
||||
*/
|
||||
private function get_gateway_mode_string() {
|
||||
if ( WC_Payments::mode()->is_dev() ) {
|
||||
return 'dev';
|
||||
} elseif ( WC_Payments::mode()->is_test() ) {
|
||||
return 'test';
|
||||
}
|
||||
return 'live';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Processes the Stripe domain registration.
|
||||
*/
|
||||
public function register_domain() {
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
$registration_response = $this->payments_api_client->register_domain( $this->domain_name );
|
||||
|
||||
if ( isset( $registration_response['id'] ) && ( isset( $registration_response['apple_pay']['status'] ) && 'active' === $registration_response['apple_pay']['status'] ) ) {
|
||||
$this->gateway->update_option( 'apple_pay_verified_domain', $this->domain_name );
|
||||
$this->gateway->update_option( 'apple_pay_domain_set', 'yes' );
|
||||
|
||||
Logger::log( __( 'Your domain has been verified with Apple Pay!', 'woocommerce-payments' ) );
|
||||
Tracker::track_admin(
|
||||
'wcpay_apple_pay_domain_registration_success',
|
||||
[
|
||||
'domain' => $this->domain_name,
|
||||
'mode' => $this->get_gateway_mode_string(),
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
} elseif ( isset( $registration_response['apple_pay']['status_details']['error_message'] ) ) {
|
||||
$error = $registration_response['apple_pay']['status_details']['error_message'];
|
||||
}
|
||||
} catch ( API_Exception $e ) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
// Display error message in notice.
|
||||
$this->apple_pay_verify_notice = $error;
|
||||
|
||||
$this->gateway->update_option( 'apple_pay_verified_domain', $this->domain_name );
|
||||
$this->gateway->update_option( 'apple_pay_domain_set', 'no' );
|
||||
|
||||
Logger::log( 'Error registering domain with Apple: ' . $error );
|
||||
Tracker::track_admin(
|
||||
'wcpay_apple_pay_domain_registration_failure',
|
||||
[
|
||||
'domain' => $this->domain_name,
|
||||
'reason' => $error,
|
||||
'mode' => $this->get_gateway_mode_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Apple Pay domain verification if proper settings are configured.
|
||||
*/
|
||||
public function verify_domain_if_configured() {
|
||||
// If Express Checkout Buttons are not enabled,
|
||||
// do not attempt to register domain.
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that domain association file will be served.
|
||||
flush_rewrite_rules();
|
||||
|
||||
// The rewrite rule method doesn't work if permalinks are set to Plain.
|
||||
// Create/update domain association file by copying it from the plugin folder as a fallback.
|
||||
$this->update_domain_association_file();
|
||||
|
||||
// Register the domain.
|
||||
$this->register_domain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally process the Apple Pay domain verification after settings are initially set.
|
||||
*
|
||||
* @param string $option Option name.
|
||||
* @param array $settings Settings array.
|
||||
*/
|
||||
public function verify_domain_on_new_settings( $option, $settings ) {
|
||||
$this->verify_domain_on_updated_settings( [], $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally process the Apple Pay domain verification after settings are updated.
|
||||
*
|
||||
* @param array $prev_settings Settings before update.
|
||||
* @param array $settings Settings after update.
|
||||
*/
|
||||
public function verify_domain_on_updated_settings( $prev_settings, $settings ) {
|
||||
// If Gateway or Express Checkout Buttons weren't enabled, then might need to verify now.
|
||||
if ( ! $this->was_enabled( $prev_settings ) ) {
|
||||
$this->verify_domain_if_configured();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display Apple Pay registration errors.
|
||||
*/
|
||||
public function display_error_notice() {
|
||||
if ( ! $this->is_enabled() || ! $this->account->get_is_live() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$empty_notice = empty( $this->apple_pay_verify_notice );
|
||||
$domain_set = $this->gateway->get_option( 'apple_pay_domain_set' );
|
||||
// Don't display error notice if verification notice is empty and
|
||||
// apple_pay_domain_set option equals to '' or 'yes'.
|
||||
if ( $empty_notice && 'no' !== $domain_set ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apple pay is enabled by default and domain verification initializes
|
||||
* when setting screen is displayed. So if domain verification is not set,
|
||||
* something went wrong so lets notify user.
|
||||
*/
|
||||
$allowed_html = [
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'title' => [],
|
||||
],
|
||||
];
|
||||
$payment_request_button_text = __( 'Express checkouts:', 'woocommerce-payments' );
|
||||
$verification_failed_without_error = __( 'Apple Pay domain verification failed.', 'woocommerce-payments' );
|
||||
$verification_failed_with_error = __( 'Apple Pay domain verification failed with the following error:', 'woocommerce-payments' );
|
||||
$check_log_text = WC_Payments_Utils::esc_interpolated_html(
|
||||
/* translators: a: Link to the logs page */
|
||||
__( 'Please check the <a>logs</a> for more details on this issue. Debug log must be enabled under <strong>Advanced settings</strong> to see recorded logs.', 'woocommerce-payments' ),
|
||||
[
|
||||
'a' => '<a href="' . admin_url( 'admin.php?page=wc-status&tab=logs' ) . '">',
|
||||
'strong' => '<strong>',
|
||||
]
|
||||
);
|
||||
$learn_more_text = WC_Payments_Utils::esc_interpolated_html(
|
||||
__( '<a>Learn more</a>.', 'woocommerce-payments' ),
|
||||
[
|
||||
'a' => '<a href="https://woocommerce.com/document/woopayments/payment-methods/apple-pay/#domain-registration" target="_blank">',
|
||||
]
|
||||
);
|
||||
|
||||
?>
|
||||
<div class="notice notice-error apple-pay-message">
|
||||
<?php if ( $empty_notice ) : ?>
|
||||
<p>
|
||||
<strong><?php echo esc_html( $payment_request_button_text ); ?></strong>
|
||||
<?php echo esc_html( $verification_failed_without_error ); ?>
|
||||
<?php echo $learn_more_text; /* @codingStandardsIgnoreLine */ ?>
|
||||
</p>
|
||||
<?php else : ?>
|
||||
<p>
|
||||
<strong><?php echo esc_html( $payment_request_button_text ); ?></strong>
|
||||
<?php echo esc_html( $verification_failed_with_error ); ?>
|
||||
<?php echo $learn_more_text; /* @codingStandardsIgnoreLine */ ?>
|
||||
</p>
|
||||
<p><i><?php echo wp_kses( make_clickable( esc_html( $this->apple_pay_verify_notice ) ), $allowed_html ); ?></i></p>
|
||||
<?php endif; ?>
|
||||
<p><?php echo $check_log_text; /* @codingStandardsIgnoreLine */ ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Blocks_Payment_Method
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
|
||||
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
|
||||
use WCPay\WC_Payments_Checkout;
|
||||
use WCPay\WooPay\WooPay_Utilities;
|
||||
|
||||
/**
|
||||
* The payment method, which allows the gateway to work with WooCommerce Blocks.
|
||||
*/
|
||||
class WC_Payments_Blocks_Payment_Method extends AbstractPaymentMethodType {
|
||||
/**
|
||||
* The gateway instance.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* WC Payments Checkout
|
||||
*
|
||||
* @var WC_Payments_Checkout
|
||||
*/
|
||||
private $wc_payments_checkout;
|
||||
|
||||
/**
|
||||
* Initializes the class.
|
||||
*/
|
||||
public function initialize() {
|
||||
$this->name = WC_Payment_Gateway_WCPay::GATEWAY_ID;
|
||||
$this->gateway = WC_Payments::get_gateway();
|
||||
$this->wc_payments_checkout = WC_Payments::get_wc_payments_checkout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the gateway is active.
|
||||
*
|
||||
* @return boolean True when active.
|
||||
*/
|
||||
public function is_active() {
|
||||
return $this->gateway->is_available();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines all scripts, necessary for the payment method.
|
||||
*
|
||||
* @return string[] A list of script handles.
|
||||
*/
|
||||
public function get_payment_method_script_handles() {
|
||||
|
||||
if ( ( is_cart() || is_checkout() || is_product() || has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) || is_admin() ) ) {
|
||||
WC_Payments_Utils::enqueue_style(
|
||||
'wc-blocks-checkout-style',
|
||||
plugins_url( 'dist/blocks-checkout.css', WCPAY_PLUGIN_FILE ),
|
||||
[],
|
||||
WC_Payments::get_file_version( 'dist/checkout.css' ),
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
wp_register_script(
|
||||
'stripe',
|
||||
'https://js.stripe.com/v3/',
|
||||
[],
|
||||
'3.0',
|
||||
true
|
||||
);
|
||||
|
||||
WC_Payments::register_script_with_dependencies( 'WCPAY_BLOCKS_CHECKOUT', 'dist/blocks-checkout', [ 'stripe' ] );
|
||||
|
||||
wp_set_script_translations( 'WCPAY_BLOCKS_CHECKOUT', 'woocommerce-payments' );
|
||||
|
||||
wp_add_inline_script(
|
||||
'WCPAY_BLOCKS_CHECKOUT',
|
||||
'var wcBlocksCheckoutData = ' . wp_json_encode(
|
||||
[
|
||||
'amount' => WC()->cart ? WC()->cart->get_total( '' ) : 0,
|
||||
'currency' => get_woocommerce_currency(),
|
||||
'storeCountry' => WC()->countries->get_base_country(),
|
||||
'billingCountry' => WC()->customer ? WC()->customer->get_billing_country() : 'US',
|
||||
]
|
||||
) . ';',
|
||||
'before'
|
||||
);
|
||||
|
||||
Fraud_Prevention_Service::maybe_append_fraud_prevention_token();
|
||||
|
||||
return [ 'WCPAY_BLOCKS_CHECKOUT' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the data about the gateway, which will be exposed in JavaScript.
|
||||
*
|
||||
* @return array An associative array, containing all necessary values.
|
||||
*/
|
||||
public function get_payment_method_data() {
|
||||
$is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag.
|
||||
$is_woopay_enabled = 'yes' === $this->gateway->get_option( 'platform_checkout', 'no' );
|
||||
$woopay_config = [];
|
||||
|
||||
if ( $is_woopay_eligible && $is_woopay_enabled ) {
|
||||
$woopay_config = [
|
||||
'woopayHost' => WooPay_Utilities::get_woopay_url(),
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
[
|
||||
'title' => $this->gateway->get_option( 'title', '' ),
|
||||
'description' => $this->gateway->get_option( 'description', '' ),
|
||||
'is_admin' => is_admin(), // Used to display payment method preview in wp-admin.
|
||||
],
|
||||
$woopay_config,
|
||||
$this->wc_payments_checkout->get_payment_fields_js_config()
|
||||
);
|
||||
}
|
||||
}
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Captured_Event_Note
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class generating detailed captured note for successful payments.
|
||||
*/
|
||||
class WC_Payments_Captured_Event_Note {
|
||||
const HTML_BLACK_BULLET = '<span style="font-size: 7px;vertical-align: middle;">●</span>';
|
||||
const HTML_WHITE_BULLET = '<span style="font-size: 7px;vertical-align: middle;">○</span>';
|
||||
const HTML_SPACE = ' ';
|
||||
const HTML_BR = '<br>';
|
||||
|
||||
/**
|
||||
* Captured event data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $captured_event;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $captured_event Captured event data.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct( array $captured_event ) {
|
||||
$is_captured_event = isset( $captured_event['type'] ) && 'captured' === $captured_event['type'];
|
||||
if ( ! $is_captured_event ) {
|
||||
throw new Exception( 'Not a captured event' );
|
||||
}
|
||||
|
||||
$this->captured_event = $captured_event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the HTML note.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate_html_note(): string {
|
||||
|
||||
$lines = [];
|
||||
|
||||
$fx_string = $this->compose_fx_string();
|
||||
if ( null !== $fx_string ) {
|
||||
$lines[] = $fx_string;
|
||||
}
|
||||
|
||||
$lines[] = $this->compose_fee_string();
|
||||
|
||||
$fee_breakdown_lines = $this->compose_fee_break_down();
|
||||
if ( null !== $fee_breakdown_lines ) {
|
||||
$lines = array_merge( $lines, $fee_breakdown_lines );
|
||||
}
|
||||
|
||||
$lines[] = $this->compose_net_string();
|
||||
|
||||
$html = '';
|
||||
foreach ( $lines as $line ) {
|
||||
$html .= '<p>' . $line . '</p>' . PHP_EOL;
|
||||
}
|
||||
|
||||
return '<div class="captured-event-details">' . PHP_EOL
|
||||
. $html
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate FX string.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function compose_fx_string() {
|
||||
if ( ! $this->is_fx_event() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customer_currency = $this->captured_event['transaction_details']['customer_currency'];
|
||||
$customer_amount_captured = $this->captured_event['transaction_details']['customer_amount_captured'];
|
||||
$store_currency = $this->captured_event['transaction_details']['store_currency'];
|
||||
$store_amount_captured = $this->captured_event['transaction_details']['store_amount_captured'];
|
||||
|
||||
return $this->format_fx( $customer_currency, $customer_amount_captured, $store_currency, $store_amount_captured );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fee string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compose_fee_string(): string {
|
||||
$data = $this->captured_event;
|
||||
|
||||
$fee_rates = $data['fee_rates'];
|
||||
$percentage = $fee_rates['percentage'];
|
||||
$fixed_currency = $fee_rates['fixed_currency'];
|
||||
$fixed = WC_Payments_Utils::interpret_stripe_amount( (int) $fee_rates['fixed'], $fixed_currency );
|
||||
$history = $fee_rates['history'];
|
||||
|
||||
$fee_currency = $data['transaction_details']['store_currency'];
|
||||
$fee_amount = WC_Payments_Utils::interpret_stripe_amount( (int) $data['transaction_details']['store_fee'], $fee_currency );
|
||||
|
||||
$base_fee_label = $this->is_base_fee_only()
|
||||
? __( 'Base fee', 'woocommerce-payments' )
|
||||
: __( 'Fee', 'woocommerce-payments' );
|
||||
|
||||
$is_capped = isset( $history[0]['capped'] ) && true === $history[0]['capped'];
|
||||
|
||||
if ( $this->is_base_fee_only() && $is_capped ) {
|
||||
return sprintf(
|
||||
'%1$s (capped at %2$s): %3$s',
|
||||
$base_fee_label,
|
||||
WC_Payments_Utils::format_currency( $fixed, $fixed_currency ),
|
||||
WC_Payments_Utils::format_currency( - $fee_amount, $fee_currency )
|
||||
);
|
||||
}
|
||||
$is_same_symbol = $this->has_same_currency_symbol( $data['transaction_details']['store_currency'], $data['transaction_details']['customer_currency'] );
|
||||
|
||||
return sprintf(
|
||||
'%1$s (%2$s%% + %3$s%4$s): %5$s%6$s',
|
||||
$base_fee_label,
|
||||
self::format_fee( $percentage ),
|
||||
WC_Payments_Utils::format_currency( $fixed, $fixed_currency ),
|
||||
$is_same_symbol ? ' ' . $data['transaction_details']['customer_currency'] : '',
|
||||
WC_Payments_Utils::format_currency( -$fee_amount, $fee_currency ),
|
||||
$is_same_symbol ? " $fee_currency" : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an array including HTML formatted breakdown lines.
|
||||
*
|
||||
* @return array<string>|null
|
||||
*/
|
||||
public function compose_fee_break_down() {
|
||||
$fee_history_strings = $this->get_fee_breakdown();
|
||||
|
||||
if ( null === $fee_history_strings ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( 0 === count( $fee_history_strings ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$res = [];
|
||||
foreach ( $fee_history_strings as $type => $fee ) {
|
||||
$res[] = self::HTML_BLACK_BULLET . ' ' . ( 'discount' === $type
|
||||
? $fee['label']
|
||||
: $fee
|
||||
);
|
||||
|
||||
if ( 'discount' === $type ) {
|
||||
$res[] = str_repeat( self::HTML_SPACE . ' ', 2 ) . self::HTML_WHITE_BULLET . ' ' . $fee['variable'];
|
||||
$res[] = str_repeat( self::HTML_SPACE . ' ', 2 ) . self::HTML_WHITE_BULLET . ' ' . $fee['fixed'];
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate net string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compose_net_string(): string {
|
||||
$data = $this->captured_event['transaction_details'];
|
||||
|
||||
// Determine the type of payment and select the appropriate amounts and currencies.
|
||||
if ( $this->is_fx_event() ) {
|
||||
// For fx events, we need the store amount and currency to display the net amount
|
||||
// in the store currency.
|
||||
$amount = $data['store_amount'];
|
||||
$captured_amount = $data['store_amount_captured'];
|
||||
$fee = $data['store_fee'];
|
||||
$currency = $data['store_currency'];
|
||||
} else {
|
||||
$amount = $data['customer_amount'];
|
||||
$captured_amount = $data['customer_amount_captured'];
|
||||
$fee = $data['customer_fee'];
|
||||
$currency = $data['customer_currency'];
|
||||
}
|
||||
|
||||
$gross_amount = $captured_amount ?? $amount;
|
||||
$net = WC_Payments_Utils::interpret_stripe_amount( (int) ( $gross_amount - $fee ), $currency );
|
||||
|
||||
// Format and return the net string.
|
||||
return sprintf(
|
||||
/* translators: %s is a monetary amount */
|
||||
__( 'Net payout: %s', 'woocommerce-payments' ),
|
||||
WC_Payments_Utils::format_explicit_currency( $net, $currency )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array containing fee breakdown.
|
||||
* Keys are fee types such as base, additional-fx, etc, except for "discount" that is an associative array including more discount details.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_fee_breakdown() {
|
||||
$data = $this->captured_event;
|
||||
|
||||
if ( ! isset( $data['fee_rates']['history'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$history = $data['fee_rates']['history'];
|
||||
|
||||
// Hide breakdown when there's only a base fee.
|
||||
if ( $this->is_base_fee_only() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fee_history_strings = [];
|
||||
|
||||
foreach ( $history as $fee ) {
|
||||
$label_type = $fee['type'];
|
||||
if ( $fee['additional_type'] ?? '' ) {
|
||||
$label_type .= '-' . $fee['additional_type'];
|
||||
}
|
||||
|
||||
$percentage_rate = (float) $fee['percentage_rate'];
|
||||
$fixed_rate = (int) $fee['fixed_rate'];
|
||||
$currency = strtoupper( $fee['currency'] );
|
||||
$is_capped = isset( $fee['capped'] ) && true === $fee['capped'];
|
||||
|
||||
$percentage_rate_formatted = self::format_fee( $percentage_rate );
|
||||
$fix_rate_formatted = WC_Payments_Utils::format_currency(
|
||||
WC_Payments_Utils::interpret_stripe_amount( $fixed_rate ),
|
||||
$currency
|
||||
);
|
||||
|
||||
if ( $this->has_same_currency_symbol( $data['transaction_details']['customer_currency'], $data['transaction_details']['store_currency'] ) ) {
|
||||
$fix_rate_formatted = $fix_rate_formatted . ' ' . $data['transaction_details']['store_currency'];
|
||||
}
|
||||
|
||||
$label = sprintf(
|
||||
$this->fee_label_mapping( $fixed_rate, $is_capped )[ $label_type ],
|
||||
$percentage_rate_formatted,
|
||||
$fix_rate_formatted
|
||||
);
|
||||
|
||||
if ( 'discount' === $label_type ) {
|
||||
$fee_history_strings[ $label_type ] = [
|
||||
'label' => $label,
|
||||
'variable' => sprintf(
|
||||
/* translators: %s is a percentage number */
|
||||
__( 'Variable fee: %s', 'woocommerce-payments' ),
|
||||
$percentage_rate_formatted
|
||||
) . '%',
|
||||
'fixed' => sprintf(
|
||||
/* translators: %s is a monetary amount */
|
||||
__( 'Fixed fee: %s', 'woocommerce-payments' ),
|
||||
$fix_rate_formatted
|
||||
),
|
||||
];
|
||||
} else {
|
||||
$fee_history_strings[ $label_type ] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $fee_history_strings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a FX event.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_fx_event(): bool {
|
||||
$customer_currency = $this->captured_event['transaction_details']['customer_currency'] ?? null;
|
||||
$store_currency = $this->captured_event['transaction_details']['store_currency'] ?? null;
|
||||
|
||||
return ! (
|
||||
is_null( $customer_currency )
|
||||
|| is_null( $store_currency )
|
||||
|| $customer_currency === $store_currency
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean indicating whether only fee applied is the base fee.
|
||||
*
|
||||
* @return bool True if the only applied fee is the base fee
|
||||
*/
|
||||
private function is_base_fee_only(): bool {
|
||||
if ( ! isset( $this->captured_event['fee_rates']['history'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$history = $this->captured_event['fee_rates']['history'];
|
||||
|
||||
return 1 === ( is_countable( $history ) ? count( $history ) : 0 ) && 'base' === $history[0]['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping format for all types of fees.
|
||||
*
|
||||
* @param int $fixed_rate Fixed rate amount in Stripe format.
|
||||
* @param bool $is_capped True if the fee is capped.
|
||||
*
|
||||
* @return array An associative array with keys are fee types, values are string formats.
|
||||
*/
|
||||
private function fee_label_mapping( int $fixed_rate, bool $is_capped ) {
|
||||
$res = [];
|
||||
|
||||
$res['base'] = $is_capped
|
||||
/* translators: %2$s is the capped fee */
|
||||
? __( 'Base fee: capped at %2$s', 'woocommerce-payments' )
|
||||
:
|
||||
( 0 !== $fixed_rate
|
||||
/* translators: %1$s% is the fee percentage and %2$s is the fixed rate */
|
||||
? __( 'Base fee: %1$s%% + %2$s', 'woocommerce-payments' )
|
||||
/* translators: %1$s% is the fee percentage */
|
||||
: __( 'Base fee: %1$s%%', 'woocommerce-payments' )
|
||||
);
|
||||
|
||||
$res['additional-international'] = 0 !== $fixed_rate
|
||||
/* translators: %1$s% is the fee percentage and %2$s is the fixed rate */
|
||||
? __( 'International card fee: %1$s%% + %2$s', 'woocommerce-payments' )
|
||||
/* translators: %1$s% is the fee percentage */
|
||||
: __( 'International card fee: %1$s%%', 'woocommerce-payments' );
|
||||
|
||||
$res['additional-fx'] = 0 !== $fixed_rate
|
||||
/* translators: %1$s% is the fee percentage and %2$s is the fixed rate */
|
||||
? __( 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' )
|
||||
/* translators: %1$s% is the fee percentage */
|
||||
: __( 'Currency conversion fee: %1$s%%', 'woocommerce-payments' );
|
||||
|
||||
$res['additional-wcpay-subscription'] = 0 !== $fixed_rate
|
||||
/* translators: %1$s% is the fee percentage and %2$s is the fixed rate */
|
||||
? __( 'Subscription transaction fee: %1$s%% + %2$s', 'woocommerce-payments' )
|
||||
/* translators: %1$s% is the fee percentage */
|
||||
: __( 'Subscription transaction fee: %1$s%%', 'woocommerce-payments' );
|
||||
|
||||
$res['discount'] = __( 'Discount', 'woocommerce-payments' );
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a given decimal fee as a percentage with a maximum of 3 decimal places.
|
||||
*
|
||||
* @param float $percentage Percentage as float.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function format_fee( float $percentage ): string {
|
||||
return (string) round( $percentage * 100, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format FX string based on the two provided currencies.
|
||||
*
|
||||
* @param string $from_currency 3-letter code for original currency.
|
||||
* @param int $from_amount Amount (Stripe-type) for original currency.
|
||||
* @param string $to_currency 3-letter code for converted currency.
|
||||
* @param int $to_amount Amount (Stripe-type) for converted currency.
|
||||
*
|
||||
* @return string Formatted FX string.
|
||||
*/
|
||||
private function format_fx(
|
||||
string $from_currency,
|
||||
int $from_amount,
|
||||
string $to_currency,
|
||||
int $to_amount
|
||||
): string {
|
||||
|
||||
$exchange_rate = (float) ( 0 !== $from_amount
|
||||
? $to_amount / $from_amount
|
||||
: 0 );
|
||||
|
||||
if ( WC_Payments_Utils::is_zero_decimal_currency( strtolower( $to_currency ) ) ) {
|
||||
$exchange_rate *= 100;
|
||||
}
|
||||
|
||||
if ( WC_Payments_Utils::is_zero_decimal_currency( strtolower( $from_currency ) ) ) {
|
||||
$exchange_rate /= 100;
|
||||
}
|
||||
|
||||
$to_display_amount = WC_Payments_Utils::interpret_stripe_amount( $to_amount, $to_currency );
|
||||
|
||||
return sprintf(
|
||||
'%1$s → %2$s: %3$s',
|
||||
self::format_explicit_currency_with_base( 1, $from_currency, $to_currency, true ),
|
||||
self::format_exchange_rate( $exchange_rate, $to_currency ),
|
||||
WC_Payments_Utils::format_explicit_currency( $to_display_amount, $to_currency, false )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format exchange rate.
|
||||
*
|
||||
* @param float $rate Exchange rate.
|
||||
* @param string $currency 3-letter currency code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function format_exchange_rate( float $rate, string $currency ): string {
|
||||
$num_decimals = $rate > 1 ? 5 : 6;
|
||||
$formatted = WC_Payments_Utils::format_explicit_currency(
|
||||
$rate,
|
||||
$currency,
|
||||
true,
|
||||
[ 'decimals' => $num_decimals ]
|
||||
);
|
||||
|
||||
$func_remove_ending_zeros = function ( $str ) {
|
||||
return rtrim( $str, '0' );
|
||||
};
|
||||
|
||||
// Remove ending zeroes after the decimal separator if they exist.
|
||||
return implode(
|
||||
' ',
|
||||
array_map(
|
||||
$func_remove_ending_zeros,
|
||||
explode( ' ', $formatted )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format amount for a given currency but according to the base currency's format.
|
||||
*
|
||||
* @param float $amount Amount.
|
||||
* @param string $currency 3-letter currency code.
|
||||
* @param string $base_currency 3-letter base currency code.
|
||||
* @param bool $skip_symbol Optional. If true, trims off the short currency symbol. Default false.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function format_explicit_currency_with_base( float $amount, string $currency, string $base_currency, bool $skip_symbol = false ) {
|
||||
$custom_format = WC_Payments_Utils::get_currency_format_for_wc_price( $base_currency );
|
||||
unset( $custom_format['currency'] );
|
||||
|
||||
// Given this is used to display the $amount, the decimals for $base_currency shouldn't interfere with decimals for $currency.
|
||||
$custom_format['decimals'] = WC_Payments_Utils::get_currency_format_for_wc_price( $currency )['decimals'];
|
||||
|
||||
return WC_Payments_Utils::format_explicit_currency( $amount, $currency, $skip_symbol, $custom_format );
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare does two currencies have the same symbol.
|
||||
*
|
||||
* @param string $base_currency Base currency.
|
||||
* @param string $currency Currency to compare.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_same_currency_symbol( string $base_currency, string $currency ): bool {
|
||||
return strcasecmp( $base_currency, $currency ) !== 0 && get_woocommerce_currency_symbol( $base_currency ) === get_woocommerce_currency_symbol( $currency );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Checkout
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use Exception;
|
||||
use Jetpack_Options;
|
||||
use WC_AJAX;
|
||||
use WC_Checkout;
|
||||
use WC_Payments;
|
||||
use WC_Payments_Account;
|
||||
use WC_Payments_Customer_Service;
|
||||
use WC_Payments_Fraud_Service;
|
||||
use WC_Payments_Utils;
|
||||
use WC_Payments_Features;
|
||||
use WCPay\Constants\Payment_Method;
|
||||
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
|
||||
use WC_Payment_Gateway_WCPay;
|
||||
use WCPay\WooPay\WooPay_Utilities;
|
||||
use WCPay\Payment_Methods\UPE_Payment_Method;
|
||||
use WCPay\WooPay\WooPay_Session;
|
||||
|
||||
/**
|
||||
* WC_Payments_Checkout
|
||||
*/
|
||||
class WC_Payments_Checkout {
|
||||
|
||||
/**
|
||||
* WC Payments Gateway.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
protected $gateway;
|
||||
|
||||
/**
|
||||
* WooPay Utilities.
|
||||
*
|
||||
* @var WooPay_Utilities
|
||||
*/
|
||||
protected $woopay_util;
|
||||
|
||||
/**
|
||||
* WC Payments Account.
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* WC Payments Customer Service
|
||||
*
|
||||
* @var WC_Payments_Customer_Service
|
||||
*/
|
||||
protected $customer_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Fraud_Service instance to get information about fraud services.
|
||||
*
|
||||
* @var WC_Payments_Fraud_Service
|
||||
*/
|
||||
protected $fraud_service;
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*
|
||||
* @param WC_Payment_Gateway_WCPay $gateway WC Payment Gateway.
|
||||
* @param WooPay_Utilities $woopay_util WooPay Utilities.
|
||||
* @param WC_Payments_Account $account WC Payments Account.
|
||||
* @param WC_Payments_Customer_Service $customer_service WC Payments Customer Service.
|
||||
* @param WC_Payments_Fraud_Service $fraud_service Fraud service instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payment_Gateway_WCPay $gateway,
|
||||
WooPay_Utilities $woopay_util,
|
||||
WC_Payments_Account $account,
|
||||
WC_Payments_Customer_Service $customer_service,
|
||||
WC_Payments_Fraud_Service $fraud_service
|
||||
) {
|
||||
$this->gateway = $gateway;
|
||||
$this->woopay_util = $woopay_util;
|
||||
$this->account = $account;
|
||||
$this->customer_service = $customer_service;
|
||||
$this->fraud_service = $fraud_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'wc_payments_set_gateway', [ $this, 'set_gateway' ] );
|
||||
add_action( 'wc_payments_add_upe_payment_fields', [ $this, 'payment_fields' ] );
|
||||
add_action( 'wp', [ $this->gateway, 'maybe_process_upe_redirect' ] );
|
||||
add_action( 'wp_ajax_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] );
|
||||
add_action( 'wp_ajax_nopriv_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] );
|
||||
add_action( 'switch_theme', [ $this->gateway, 'clear_upe_appearance_transient' ] );
|
||||
add_action( 'woocommerce_woocommerce_payments_updated', [ $this->gateway, 'clear_upe_appearance_transient' ] );
|
||||
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 );
|
||||
add_action( 'woocommerce_after_checkout_form', [ $this, 'maybe_load_checkout_scripts' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all scripts, necessary for the gateway.
|
||||
*/
|
||||
public function register_scripts() {
|
||||
if ( wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) {
|
||||
return;
|
||||
}
|
||||
// Register Stripe's JavaScript using the same ID as the Stripe Gateway plugin. This prevents this JS being
|
||||
// loaded twice in the event a site has both plugins enabled. We still run the risk of different plugins
|
||||
// loading different versions however. If Stripe release a v4 of their JavaScript, we could consider
|
||||
// changing the ID to stripe_v4. This would allow older plugins to keep using v3 while we used any new
|
||||
// feature in v4. Stripe have allowed loading of 2 different versions of stripe.js in the past (
|
||||
// https://stripe.com/docs/stripe-js/elements/migrating).
|
||||
wp_register_script(
|
||||
'stripe',
|
||||
'https://js.stripe.com/v3/',
|
||||
[],
|
||||
'3.0',
|
||||
true
|
||||
);
|
||||
|
||||
$script_dependencies = [ 'stripe', 'wc-checkout', 'wp-i18n' ];
|
||||
|
||||
if ( $this->gateway->supports( 'tokenization' ) ) {
|
||||
$script_dependencies[] = 'woocommerce-tokenization-form';
|
||||
}
|
||||
|
||||
Fraud_Prevention_Service::maybe_append_fraud_prevention_token();
|
||||
|
||||
WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', 'dist/checkout', $script_dependencies );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers scripts necessary for the gateway, even when cart order total is 0.
|
||||
* This is done so that if the cart is modified via AJAX on checkout,
|
||||
* the scripts are still loaded.
|
||||
*/
|
||||
public function register_scripts_for_zero_order_total() {
|
||||
if (
|
||||
isset( WC()->cart ) &&
|
||||
! WC()->cart->is_empty() &&
|
||||
! WC()->cart->needs_payment() &&
|
||||
is_checkout() &&
|
||||
! has_block( 'woocommerce/checkout' ) &&
|
||||
! wp_script_is( 'wcpay-upe-checkout', 'enqueued' )
|
||||
) {
|
||||
$this->load_checkout_scripts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sometimes the filters can remove the payment gateway from the checkout page which results in the payment fields not being displayed.
|
||||
* This could prevent loading of the payment fields (checkout) scripts.
|
||||
* This function ensures that these scripts are loaded.
|
||||
*/
|
||||
public function maybe_load_checkout_scripts() {
|
||||
if ( is_checkout() && ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) {
|
||||
$this->load_checkout_scripts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the configuration values, needed for payment fields.
|
||||
*
|
||||
* Isolated as a separate method in order to be available both
|
||||
* during the classic checkout, as well as the checkout block.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_payment_fields_js_config() {
|
||||
// Needed to init the hooks.
|
||||
WC_Checkout::instance();
|
||||
|
||||
// The registered card gateway is more reliable than $this->gateway, but if it isn't available for any reason, fall back to the gateway provided to this checkout class.
|
||||
$gateway = WC_Payments::get_gateway() ?? $this->gateway;
|
||||
|
||||
$js_config = [
|
||||
'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ),
|
||||
'testMode' => WC_Payments::mode()->is_test(),
|
||||
'accountId' => $this->account->get_stripe_account_id(),
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
|
||||
'createSetupIntentNonce' => wp_create_nonce( 'wcpay_create_setup_intent_nonce' ),
|
||||
'initWooPayNonce' => wp_create_nonce( 'wcpay_init_woopay_nonce' ),
|
||||
'saveUPEAppearanceNonce' => wp_create_nonce( 'wcpay_save_upe_appearance_nonce' ),
|
||||
'genericErrorMessage' => __( 'There was a problem processing the payment. Please check your email inbox and refresh the page to try again.', 'woocommerce-payments' ),
|
||||
'fraudServices' => $this->fraud_service->get_fraud_services_config(),
|
||||
'features' => $this->gateway->supports,
|
||||
'forceNetworkSavedCards' => WC_Payments::is_network_saved_cards_enabled() || $gateway->should_use_stripe_platform_on_checkout_page(),
|
||||
'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ),
|
||||
'isPreview' => is_preview(),
|
||||
'isSavedCardsEnabled' => $this->gateway->is_saved_cards_enabled(),
|
||||
'isPaymentRequestEnabled' => $this->gateway->is_payment_request_enabled(),
|
||||
'isTokenizedCartEceEnabled' => WC_Payments_Features::is_tokenized_cart_ece_enabled(),
|
||||
'isWooPayEnabled' => $this->woopay_util->should_enable_woopay( $this->gateway ) && $this->woopay_util->should_enable_woopay_on_cart_or_checkout(),
|
||||
'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(),
|
||||
'isWoopayFirstPartyAuthEnabled' => $this->woopay_util->is_woopay_first_party_auth_enabled(),
|
||||
'isWooPayEmailInputEnabled' => $this->woopay_util->is_woopay_email_input_enabled(),
|
||||
'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(),
|
||||
'isWooPayGlobalThemeSupportEnabled' => $this->gateway->is_woopay_global_theme_support_enabled(),
|
||||
'woopayHost' => WooPay_Utilities::get_woopay_url(),
|
||||
'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ),
|
||||
'accountIdForIntentConfirmation' => apply_filters( 'wc_payments_account_id_for_intent_confirmation', '' ),
|
||||
'wcpayVersionNumber' => WCPAY_VERSION_NUMBER,
|
||||
'woopaySignatureNonce' => wp_create_nonce( 'woopay_signature_nonce' ),
|
||||
'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ),
|
||||
'woopayMerchantId' => Jetpack_Options::get_option( 'id' ),
|
||||
'icon' => $this->gateway->get_icon_url(),
|
||||
'woopayMinimumSessionData' => WooPay_Session::get_woopay_minimum_session_data(),
|
||||
];
|
||||
|
||||
/**
|
||||
* Allows filtering of the JS config for the payment fields.
|
||||
*
|
||||
* @param array $js_config The JS config for the payment fields.
|
||||
*/
|
||||
$payment_fields = apply_filters( 'wcpay_payment_fields_js_config', $js_config );
|
||||
|
||||
$payment_fields['accountDescriptor'] = $this->gateway->get_account_statement_descriptor();
|
||||
$payment_fields['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' );
|
||||
$payment_fields['gatewayId'] = WC_Payment_Gateway_WCPay::GATEWAY_ID;
|
||||
$payment_fields['isCheckout'] = is_checkout();
|
||||
$payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config();
|
||||
$payment_fields['testMode'] = WC_Payments::mode()->is_test();
|
||||
$payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT );
|
||||
$payment_fields['upeAddPaymentMethodAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT );
|
||||
$payment_fields['upeBnplProductPageAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT );
|
||||
$payment_fields['upeBnplClassicCartAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT );
|
||||
$payment_fields['upeBnplCartBlockAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT );
|
||||
$payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
|
||||
$payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT );
|
||||
$payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart();
|
||||
$payment_fields['currency'] = get_woocommerce_currency();
|
||||
$cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 );
|
||||
$payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() );
|
||||
|
||||
$enabled_billing_fields = [];
|
||||
foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) {
|
||||
if ( ! isset( $billing_field_options['enabled'] ) || $billing_field_options['enabled'] ) {
|
||||
$enabled_billing_fields[ $billing_field ] = [
|
||||
'required' => $billing_field_options['required'],
|
||||
];
|
||||
}
|
||||
}
|
||||
$payment_fields['enabledBillingFields'] = $enabled_billing_fields;
|
||||
|
||||
if ( is_wc_endpoint_url( 'order-pay' ) ) {
|
||||
if ( $this->gateway->is_subscriptions_enabled() && $this->gateway->is_changing_payment_method_for_subscription() ) {
|
||||
$payment_fields['isChangingPayment'] = true;
|
||||
$payment_fields['addPaymentReturnURL'] = esc_url_raw( home_url( add_query_arg( [] ) ) );
|
||||
|
||||
if ( $this->gateway->is_setup_intent_success_creation_redirection() && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
|
||||
$setup_intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
|
||||
$token = $this->gateway->create_token_from_setup_intent( $setup_intent_id, wp_get_current_user() );
|
||||
if ( null !== $token ) {
|
||||
$payment_fields['newTokenFormId'] = '#wc-' . $token->get_gateway_id() . '-payment-token-' . $token->get_id();
|
||||
}
|
||||
}
|
||||
return $payment_fields; // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in.
|
||||
}
|
||||
|
||||
$payment_fields['isOrderPay'] = true;
|
||||
$order_id = absint( get_query_var( 'order-pay' ) );
|
||||
$payment_fields['orderId'] = $order_id;
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( is_a( $order, 'WC_Order' ) ) {
|
||||
$order_currency = $order->get_currency();
|
||||
$payment_fields['currency'] = $order_currency;
|
||||
$payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $order->get_total(), $order_currency );
|
||||
$payment_fields['orderReturnURL'] = esc_url_raw(
|
||||
add_query_arg(
|
||||
[
|
||||
'wc_payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID,
|
||||
'_wpnonce' => wp_create_nonce( 'wcpay_process_redirect_order_nonce' ),
|
||||
],
|
||||
$this->gateway->get_return_url( $order )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the store base country.
|
||||
$payment_fields['storeCountry'] = WC()->countries->get_base_country();
|
||||
|
||||
// Get the WooCommerce Store API endpoint.
|
||||
$payment_fields['storeApiURL'] = get_rest_url( null, 'wc/store' );
|
||||
|
||||
/**
|
||||
* Allows filtering for the payment fields.
|
||||
*
|
||||
* @param array $payment_fields The payment fields.
|
||||
*/
|
||||
return apply_filters( 'wcpay_payment_fields_js_config', $payment_fields ); // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in.
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets payment method settings to pass to client scripts
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_enabled_payment_method_config() {
|
||||
$settings = [];
|
||||
$enabled_payment_methods = $this->gateway->get_payment_method_ids_enabled_at_checkout();
|
||||
|
||||
foreach ( $enabled_payment_methods as $payment_method_id ) {
|
||||
// Link by Stripe should be validated with available fees.
|
||||
if ( Payment_Method::LINK === $payment_method_id ) {
|
||||
if ( ! in_array( Payment_Method::LINK, array_keys( $this->account->get_fees() ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$payment_method = $this->gateway->wc_payments_get_payment_method_by_id( $payment_method_id );
|
||||
$account_country = $this->account->get_account_country();
|
||||
$settings[ $payment_method_id ] = [
|
||||
'isReusable' => $payment_method->is_reusable(),
|
||||
'title' => $payment_method->get_title( $account_country ),
|
||||
'icon' => $payment_method->get_icon( $account_country ),
|
||||
'darkIcon' => $payment_method->get_dark_icon( $account_country ),
|
||||
'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ),
|
||||
'countries' => $payment_method->get_countries(),
|
||||
];
|
||||
|
||||
$gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id );
|
||||
$settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html(
|
||||
/* translators: link to Stripe testing page */
|
||||
$payment_method->get_testing_instructions( $account_country ),
|
||||
[
|
||||
'a' => '<a href="https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" target="_blank">',
|
||||
'strong' => '<strong>',
|
||||
'number' => '<button type="button" class="js-woopayments-copy-test-number" aria-label="' . esc_attr( __( 'Click to copy the test number to clipboard', 'woocommerce-payments' ) ) . '" title="' . esc_attr( __( 'Copy to clipboard', 'woocommerce-payments' ) ) . '"><i></i><span>',
|
||||
]
|
||||
);
|
||||
|
||||
$should_enable_network_saved_cards = Payment_Method::CARD === $payment_method_id && WC_Payments::is_network_saved_cards_enabled();
|
||||
$settings[ $payment_method_id ]['forceNetworkSavedCards'] = $should_enable_network_saved_cards || $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page();
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the save option for a payment method should be displayed or not.
|
||||
*
|
||||
* @param UPE_Payment_Method $payment_method UPE Payment Method instance.
|
||||
* @return bool - True if the payment method is reusable and the saved cards feature is enabled for the gateway and there is no subscription item in the cart, false otherwise.
|
||||
*/
|
||||
private function should_upe_payment_method_show_save_option( $payment_method ) {
|
||||
if ( $payment_method->get_id() === Payment_Method::CARD && is_user_logged_in() && WC_Payments_Features::is_woopay_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $payment_method->is_reusable() ) {
|
||||
return $this->gateway->is_saved_cards_enabled() && ! $this->gateway->is_subscription_item_in_cart();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the UPE input fields needed to get the user's payment information on the checkout page.
|
||||
*
|
||||
* We also add the JavaScript which drives the UI.
|
||||
*/
|
||||
public function payment_fields() {
|
||||
try {
|
||||
$display_tokenization = $this->gateway->supports( 'tokenization' ) && ( is_checkout() || is_add_payment_method_page() );
|
||||
|
||||
/**
|
||||
* Localizing scripts within shortcodes does not work in WP 5.9,
|
||||
* but we need `$this->get_payment_fields_js_config` to be called
|
||||
* before `$this->saved_payment_methods()`.
|
||||
*/
|
||||
if ( ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) {
|
||||
$payment_fields = $this->get_payment_fields_js_config();
|
||||
wp_enqueue_script( 'wcpay-upe-checkout' );
|
||||
/**
|
||||
* We can't localize the script right away since at this point is not registered yet.
|
||||
* We also need to make sure it that it only runs once (using a dummy action), even if
|
||||
* there are multiple payment methods available; otherwise the data will be overwritten
|
||||
* which is pointless.
|
||||
*
|
||||
* The same applies for `wcpayCustomerData` a few lines below.
|
||||
*/
|
||||
add_action(
|
||||
'wp_footer',
|
||||
function () use ( $payment_fields ) {
|
||||
if ( ! did_action( '__wcpay_upe_config_localized' ) ) {
|
||||
wp_localize_script( 'wcpay-upe-checkout', 'wcpay_upe_config', $payment_fields );
|
||||
}
|
||||
do_action( '__wcpay_upe_config_localized' );
|
||||
}
|
||||
);
|
||||
|
||||
$prepared_customer_data = $this->customer_service->get_prepared_customer_data();
|
||||
if ( ! empty( $prepared_customer_data ) ) {
|
||||
add_action(
|
||||
'wp_footer',
|
||||
function () use ( $prepared_customer_data ) {
|
||||
if ( ! did_action( '__wcpay_customer_data_localized' ) ) {
|
||||
wp_localize_script( 'wcpay-upe-checkout', 'wcpayCustomerData', $prepared_customer_data );
|
||||
}
|
||||
do_action( '__wcpay_customer_data_localized' );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
WC_Payments_Utils::enqueue_style(
|
||||
'wcpay-upe-checkout',
|
||||
plugins_url( 'dist/checkout.css', WCPAY_PLUGIN_FILE ),
|
||||
[],
|
||||
WC_Payments::get_file_version( 'dist/checkout.css' ),
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="wcpay-upe-form"
|
||||
data-payment-method-type="<?php echo esc_attr( $this->gateway->get_stripe_id() ); ?>"
|
||||
>
|
||||
<?php
|
||||
|
||||
// Output the form HTML.
|
||||
if ( ! empty( $this->gateway->get_description() ) ) :
|
||||
?>
|
||||
<p><?php echo wp_kses_post( $this->gateway->get_description() ); ?></p>
|
||||
<?php
|
||||
endif;
|
||||
|
||||
if ( WC_Payments::mode()->is_test() && false !== $this->gateway->get_payment_method()->get_testing_instructions( $this->account->get_account_country() ) ) :
|
||||
?>
|
||||
<p class="testmode-info">
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo WC_Payments_Utils::esc_interpolated_html(
|
||||
/* translators: link to Stripe testing page */
|
||||
$this->gateway->get_payment_method()->get_testing_instructions( $this->account->get_account_country() ),
|
||||
[
|
||||
'a' => '<a href="https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" target="_blank">',
|
||||
'strong' => '<strong>',
|
||||
'number' => '<button type="button" class="js-woopayments-copy-test-number" aria-label="' . esc_attr( __( 'Click to copy the test number to clipboard', 'woocommerce-payments' ) ) . '" title="' . esc_attr( __( 'Copy to clipboard', 'woocommerce-payments' ) ) . '"><i></i><span>',
|
||||
]
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
endif;
|
||||
|
||||
if ( $display_tokenization ) {
|
||||
$this->gateway->tokenization_script();
|
||||
// avoid showing saved payment methods on my-accounts add payment method page.
|
||||
if ( ! is_add_payment_method_page() ) {
|
||||
$this->gateway->saved_payment_methods();
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<fieldset style="padding: 7px" class="wc-payment-form">
|
||||
<?php
|
||||
$this->gateway->display_gateway_html();
|
||||
if ( $this->gateway->is_saved_cards_enabled() && $this->gateway->should_support_saved_payments() ) {
|
||||
$force_save_payment = ( $display_tokenization && ! apply_filters( 'wc_payments_display_save_payment_method_checkbox', $display_tokenization ) ) || is_add_payment_method_page();
|
||||
if ( is_user_logged_in() || $force_save_payment ) {
|
||||
$this->gateway->save_payment_method_checkbox( $force_save_payment );
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
do_action( 'wcpay_payment_fields_upe', $this->gateway->id );
|
||||
|
||||
} catch ( \Exception $e ) {
|
||||
// Output the error message.
|
||||
Logger::log( 'Error: ' . $e->getMessage() );
|
||||
?>
|
||||
<div>
|
||||
<?php
|
||||
echo esc_html__( 'An error was encountered when preparing the payment form. Please try again later.', 'woocommerce-payments' );
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gateway
|
||||
*
|
||||
* @param string $payment_method_id Payment method ID.
|
||||
* @return void
|
||||
*/
|
||||
public function set_gateway( $payment_method_id ) {
|
||||
if ( null !== $payment_method_id ) {
|
||||
$this->gateway = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the checkout scripts.
|
||||
*/
|
||||
private function load_checkout_scripts() {
|
||||
WC_Payments::get_gateway()->tokenization_script();
|
||||
$script_handle = 'wcpay-upe-checkout';
|
||||
$js_object = 'wcpay_upe_config';
|
||||
wp_localize_script( $script_handle, $js_object, WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() );
|
||||
wp_enqueue_script( $script_handle );
|
||||
}
|
||||
}
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Customer
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Database_Cache;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
use WCPay\Logger;
|
||||
use WCPay\Constants\Payment_Method;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class handling any customer functionality
|
||||
*/
|
||||
class WC_Payments_Customer_Service {
|
||||
/**
|
||||
* Deprecated Stripe customer ID option.
|
||||
*
|
||||
* This option was used to store the customer_id in a WC_User options before we decoupled live and test customers.
|
||||
*/
|
||||
const DEPRECATED_WCPAY_CUSTOMER_ID_OPTION = '_wcpay_customer_id';
|
||||
|
||||
/**
|
||||
* Live Stripe customer ID option.
|
||||
*
|
||||
* This option is used to store new live mode customers in a WC_User options. Customers stored in the deprecated
|
||||
* option are migrated to this one.
|
||||
*/
|
||||
const WCPAY_LIVE_CUSTOMER_ID_OPTION = '_wcpay_customer_id_live';
|
||||
|
||||
/**
|
||||
* Test Stripe customer ID option.
|
||||
*
|
||||
* This option is used to store new test mode customer IDs in a WC_User options.
|
||||
*/
|
||||
const WCPAY_TEST_CUSTOMER_ID_OPTION = '_wcpay_customer_id_test';
|
||||
|
||||
/**
|
||||
* Key used to store customer id for non logged in users in WooCommerce Session.
|
||||
*/
|
||||
const CUSTOMER_ID_SESSION_KEY = 'wcpay_customer_id';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* WC_Payments_Account instance to get information about the account
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* Database_Cache instance to get information about the account
|
||||
*
|
||||
* @var Database_Cache
|
||||
*/
|
||||
private $database_cache;
|
||||
|
||||
/**
|
||||
* WC_Payments_Session_Service instance for working with session information
|
||||
*
|
||||
* @var WC_Payments_Session_Service
|
||||
*/
|
||||
private $session_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Order_Service instance
|
||||
*
|
||||
* @var WC_Payments_Order_Service
|
||||
*/
|
||||
private $order_service;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client Payments API client.
|
||||
* @param WC_Payments_Account $account WC_Payments_Account instance.
|
||||
* @param Database_Cache $database_cache Database_Cache instance.
|
||||
* @param WC_Payments_Session_Service $session_service Session Service class instance.
|
||||
* @param WC_Payments_Order_Service $order_service Order Service class instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $payments_api_client,
|
||||
WC_Payments_Account $account,
|
||||
Database_Cache $database_cache,
|
||||
WC_Payments_Session_Service $session_service,
|
||||
WC_Payments_Order_Service $order_service
|
||||
) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
$this->account = $account;
|
||||
$this->database_cache = $database_cache;
|
||||
$this->session_service = $session_service;
|
||||
$this->order_service = $order_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*/
|
||||
public function init_hooks() {
|
||||
/*
|
||||
* Adds the WooCommerce Payments customer ID found in the user session
|
||||
* to the WordPress user as metadata.
|
||||
*
|
||||
* This is helpful in scenarios where the shopper begins the checkout flow
|
||||
* logged out (i.e. guest checkout) and a user account is created for them
|
||||
* during checkout.
|
||||
*
|
||||
* This occurs when a user account is necessary for checkout, e.g. when the shopper
|
||||
* purchases a subscription product.
|
||||
*/
|
||||
add_action( 'woocommerce_created_customer', [ $this, 'add_customer_id_to_user' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WCPay customer ID for the given WordPress user ID
|
||||
*
|
||||
* @param int|null $user_id The user ID to look for a customer ID with.
|
||||
*
|
||||
* @return string|null WCPay customer ID or null if not found.
|
||||
*/
|
||||
public function get_customer_id_by_user_id( $user_id ) {
|
||||
// User ID might be 0 if fetched from a WP_User instance for a user who isn't logged in.
|
||||
if ( null === $user_id || 0 === $user_id ) {
|
||||
// Try to retrieve the customer id from the session if stored previously.
|
||||
$customer_id = WC()->session ? WC()->session->get( self::CUSTOMER_ID_SESSION_KEY ) : null;
|
||||
return is_string( $customer_id ) ? $customer_id : null;
|
||||
}
|
||||
|
||||
$customer_id = get_user_option( $this->get_customer_id_option(), $user_id );
|
||||
|
||||
// If customer_id is false it could mean that it hasn't been migrated from the deprecated key.
|
||||
if ( false === $customer_id ) {
|
||||
$this->maybe_migrate_deprecated_customer( $user_id );
|
||||
// Customer might've been migrated in maybe_migrate_deprecated_customer, so we need to fetch it again.
|
||||
$customer_id = get_user_option( $this->get_customer_id_option(), $user_id );
|
||||
}
|
||||
|
||||
return $customer_id ? $customer_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a customer and associate it with a WordPress user.
|
||||
*
|
||||
* @param WP_User|null $user User to create a customer for.
|
||||
* @param array $customer_data Customer data.
|
||||
*
|
||||
* @return string The created customer's ID
|
||||
*
|
||||
* @throws API_Exception Error creating customer.
|
||||
*/
|
||||
public function create_customer_for_user( ?WP_User $user, array $customer_data = [] ): string {
|
||||
// Include the session ID for the user.
|
||||
$customer_data['session_id'] = $this->session_service->get_sift_session_id() ?? null;
|
||||
|
||||
// Create a customer on the WCPay server.
|
||||
$customer_id = $this->payments_api_client->create_customer( $customer_data );
|
||||
|
||||
if ( $user instanceof WP_User && $user->ID > 0 ) {
|
||||
$this->update_user_customer_id( $user->ID, $customer_id );
|
||||
}
|
||||
|
||||
if ( isset( WC()->session ) ) {
|
||||
// Save the customer id in the session for non logged in users to reuse it in payments.
|
||||
WC()->session->set( self::CUSTOMER_ID_SESSION_KEY, $customer_id );
|
||||
}
|
||||
|
||||
return $customer_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages customer details held on WCPay server for WordPress user associated with an order.
|
||||
*
|
||||
* @param int|null $user_id ID of the WP user to associate with the customer.
|
||||
* @param WC_Order $order Woo Order.
|
||||
*
|
||||
* @return string WooPayments customer ID.
|
||||
* @throws API_Exception Throws when server API request fails.
|
||||
*/
|
||||
public function get_or_create_customer_id_from_order( ?int $user_id, WC_Order $order ): string {
|
||||
// Determine the customer making the payment, create one if we don't have one already.
|
||||
$customer_id = $this->get_customer_id_by_user_id( $user_id );
|
||||
$customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ?? 0 ) );
|
||||
$user = null === $user_id ? null : get_user_by( 'id', $user_id );
|
||||
|
||||
if ( null !== $customer_id ) {
|
||||
$this->update_customer_for_user( $customer_id, $user, $customer_data );
|
||||
return $customer_id;
|
||||
}
|
||||
return $this->create_customer_for_user( $user, $customer_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the customer details held on the WCPay server associated with the given WordPress user.
|
||||
*
|
||||
* @param string $customer_id WCPay customer ID.
|
||||
* @param WP_User|null $user WordPress user.
|
||||
* @param array $customer_data Customer data.
|
||||
*
|
||||
* @return string The updated customer's ID. Can be different to the ID parameter if the customer was re-created.
|
||||
*
|
||||
* @throws API_Exception Error updating the customer.
|
||||
*/
|
||||
public function update_customer_for_user( string $customer_id, ?WP_User $user, array $customer_data ): string {
|
||||
try {
|
||||
// Update the customer on the WCPay server.
|
||||
$this->payments_api_client->update_customer(
|
||||
$customer_id,
|
||||
$customer_data
|
||||
);
|
||||
|
||||
// We successfully updated the existing customer, so return the passed in ID unchanged.
|
||||
return $customer_id;
|
||||
} catch ( API_Exception $e ) {
|
||||
// If we failed to find the customer we wanted to update, then create a new customer and associate it to the
|
||||
// current user instead. This might happen if the customer was deleted from the server, the linked WCPay
|
||||
// account was changed, or if users were imported from another site.
|
||||
if ( $e->get_error_code() === 'resource_missing' ) {
|
||||
// Create a new customer to associate with this user. We'll return the new customer ID.
|
||||
return $this->recreate_customer( $user, $customer_data );
|
||||
}
|
||||
|
||||
// For any other type of exception, just re-throw.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a payment method as default for a customer.
|
||||
*
|
||||
* @param string $customer_id The customer ID.
|
||||
* @param string $payment_method_id The payment method ID.
|
||||
*/
|
||||
public function set_default_payment_method_for_customer( $customer_id, $payment_method_id ) {
|
||||
$this->payments_api_client->update_customer(
|
||||
$customer_id,
|
||||
[
|
||||
'invoice_settings' => [
|
||||
'default_payment_method' => $payment_method_id,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all payment methods for a customer.
|
||||
*
|
||||
* @param string $customer_id The customer ID.
|
||||
* @param string $type Type of payment methods to fetch.
|
||||
*
|
||||
* @throws API_Exception We only handle 'resource_missing' code types and rethrow anything else.
|
||||
*/
|
||||
public function get_payment_methods_for_customer( $customer_id, $type = 'card' ) {
|
||||
if ( ! $customer_id ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cache_payment_methods = ! WC_Payments::is_network_saved_cards_enabled();
|
||||
$cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $type;
|
||||
|
||||
if ( $cache_payment_methods ) {
|
||||
$payment_methods = $this->database_cache->get( $cache_key );
|
||||
if ( is_array( $payment_methods ) ) {
|
||||
return $payment_methods;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$payment_methods = $this->payments_api_client->get_payment_methods( $customer_id, $type )['data'];
|
||||
if ( $cache_payment_methods ) {
|
||||
$this->database_cache->add( $cache_key, $payment_methods );
|
||||
}
|
||||
return $payment_methods;
|
||||
|
||||
} catch ( API_Exception $e ) {
|
||||
// If we failed to find the payment methods, we can simply return empty payment methods as this customer
|
||||
// will be recreated when the user successfully adds a payment method.
|
||||
if ( $e->get_error_code() === 'resource_missing' ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Rethrow for error codes we don't care about in this function.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a customer payment method.
|
||||
*
|
||||
* @param string $payment_method_id The payment method ID.
|
||||
* @param WC_Order $order Order to be used on the update.
|
||||
*/
|
||||
public function update_payment_method_with_billing_details_from_order( $payment_method_id, $order ) {
|
||||
$billing_details = $this->order_service->get_billing_data_from_order( $order );
|
||||
|
||||
if ( ! empty( $billing_details ) ) {
|
||||
$this->payments_api_client->update_payment_method(
|
||||
$payment_method_id,
|
||||
[
|
||||
'billing_details' => $billing_details,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear payment methods cache for a user.
|
||||
*
|
||||
* @param int $user_id WC user ID.
|
||||
*/
|
||||
public function clear_cached_payment_methods_for_user( $user_id ) {
|
||||
if ( WC_Payments::is_network_saved_cards_enabled() ) {
|
||||
return; // No need to do anything, payment methods will never be cached in this case.
|
||||
}
|
||||
|
||||
$retrievable_payment_method_types = [ Payment_Method::CARD, Payment_Method::LINK, Payment_Method::SEPA ];
|
||||
$customer_id = $this->get_customer_id_by_user_id( $user_id );
|
||||
foreach ( $retrievable_payment_method_types as $type ) {
|
||||
$this->database_cache->delete( Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $type );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a WC_Order or WC_Customer, returns an array representing a Stripe customer object.
|
||||
* At least one parameter has to not be null.
|
||||
*
|
||||
* @param WC_Order $wc_order The Woo order to parse.
|
||||
* @param WC_Customer $wc_customer The Woo customer to parse.
|
||||
*
|
||||
* @return array Customer data.
|
||||
*/
|
||||
public static function map_customer_data( ?WC_Order $wc_order = null, ?WC_Customer $wc_customer = null ): array {
|
||||
if ( null === $wc_customer && null === $wc_order ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Where available, the order data takes precedence over the customer.
|
||||
$object_to_parse = $wc_order ?? $wc_customer;
|
||||
$name = $object_to_parse->get_billing_first_name() . ' ' . $object_to_parse->get_billing_last_name();
|
||||
$description = '';
|
||||
if ( null !== $wc_customer && ! empty( $wc_customer->get_username() ) ) {
|
||||
// We have a logged in user, so add their username to the customer description.
|
||||
// translators: %1$s Name, %2$s Username.
|
||||
$description = sprintf( __( 'Name: %1$s, Username: %2$s', 'woocommerce-payments' ), $name, $wc_customer->get_username() );
|
||||
} else {
|
||||
// Current user is not logged in.
|
||||
// translators: %1$s Name.
|
||||
$description = sprintf( __( 'Name: %1$s, Guest', 'woocommerce-payments' ), $name );
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'email' => $object_to_parse->get_billing_email(),
|
||||
'phone' => $object_to_parse->get_billing_phone(),
|
||||
'address' => [
|
||||
'line1' => $object_to_parse->get_billing_address_1(),
|
||||
'line2' => $object_to_parse->get_billing_address_2(),
|
||||
'postal_code' => $object_to_parse->get_billing_postcode(),
|
||||
'city' => $object_to_parse->get_billing_city(),
|
||||
'state' => $object_to_parse->get_billing_state(),
|
||||
'country' => $object_to_parse->get_billing_country(),
|
||||
],
|
||||
];
|
||||
|
||||
if ( ! empty( $object_to_parse->get_shipping_postcode() ) ) {
|
||||
$data['shipping'] = [
|
||||
'name' => $object_to_parse->get_shipping_first_name() . ' ' . $object_to_parse->get_shipping_last_name(),
|
||||
'address' => [
|
||||
'line1' => $object_to_parse->get_shipping_address_1(),
|
||||
'line2' => $object_to_parse->get_shipping_address_2(),
|
||||
'postal_code' => $object_to_parse->get_shipping_postcode(),
|
||||
'city' => $object_to_parse->get_shipping_city(),
|
||||
'state' => $object_to_parse->get_shipping_state(),
|
||||
'country' => $object_to_parse->get_shipping_country(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all saved payment methods that are stored inside the database cache driver.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete_cached_payment_methods() {
|
||||
$this->database_cache->delete_by_prefix( Database_Cache::PAYMENT_METHODS_KEY_PREFIX );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates the customer for this user.
|
||||
*
|
||||
* @param WP_User|null $user User to recreate a customer for.
|
||||
* @param array $customer_data Customer data.
|
||||
*
|
||||
* @return string The newly created customer's ID
|
||||
*
|
||||
* @throws API_Exception Error creating customer.
|
||||
*/
|
||||
private function recreate_customer( ?WP_User $user, array $customer_data ): string {
|
||||
if ( $user instanceof WP_User && $user->ID > 0 ) {
|
||||
$result = delete_user_option( $user->ID, $this->get_customer_id_option() );
|
||||
if ( ! $result ) {
|
||||
// Log the error, but continue since we'll be trying to update this option in create_customer.
|
||||
Logger::error( 'Failed to delete old customer ID for user ' . $user->ID );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->create_customer_for_user( $user, $customer_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the customer option meta, taking test mode into account.
|
||||
*
|
||||
* @return string The customer ID option name.
|
||||
*/
|
||||
private function get_customer_id_option(): string {
|
||||
return WC_Payments::mode()->is_test()
|
||||
? self::WCPAY_TEST_CUSTOMER_ID_OPTION
|
||||
: self::WCPAY_LIVE_CUSTOMER_ID_OPTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate any customer ID that might be in the DEPRECATED_WCPAY_CUSTOMER_ID_OPTION.
|
||||
*
|
||||
* @param int $user_id The user ID to look for a customer ID with.
|
||||
*/
|
||||
private function maybe_migrate_deprecated_customer( $user_id ) {
|
||||
$customer_id = get_user_option( self::DEPRECATED_WCPAY_CUSTOMER_ID_OPTION, $user_id );
|
||||
if ( false !== $customer_id ) {
|
||||
// A customer was found in the deprecated key. Migrate it to the appropriate one and delete the old meta.
|
||||
// If an account is live mode, we optimistically assume that the customer is live mode, to avoid losing
|
||||
// live mode customer data. If the account is not live mode, it can only have test mode objects, so we
|
||||
// can safely migrate them to the test key.
|
||||
// If is_live cannot be determined, default it to true to avoid considering a live account as test.
|
||||
$account_is_live = null === $this->account->get_is_live() || $this->account->get_is_live();
|
||||
$customer_option_id = $account_is_live
|
||||
? self::WCPAY_LIVE_CUSTOMER_ID_OPTION
|
||||
: self::WCPAY_TEST_CUSTOMER_ID_OPTION;
|
||||
if ( update_user_option( $user_id, $customer_option_id, $customer_id ) ) {
|
||||
delete_user_option( $user_id, self::DEPRECATED_WCPAY_CUSTOMER_ID_OPTION );
|
||||
} else {
|
||||
Logger::error( 'Failed to store new customer ID for user ' . $user_id . '; legacy customer was kept.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WCPay customer ID associated with an order, or create one if none found.
|
||||
*
|
||||
* @param WC_Order $order WC Order object.
|
||||
*
|
||||
* @return string|null WCPay customer ID.
|
||||
* @throws API_Exception If there's an error creating customer.
|
||||
*/
|
||||
public function get_customer_id_for_order( $order ) {
|
||||
$customer_id = null;
|
||||
$user = $order->get_user();
|
||||
|
||||
if ( false !== $user ) {
|
||||
// Determine the customer making the payment, create one if we don't have one already.
|
||||
$customer_id = $this->get_customer_id_by_user_id( $user->ID );
|
||||
|
||||
if ( null === $customer_id ) {
|
||||
$customer_data = self::map_customer_data( $order, new WC_Customer( $user->ID ) );
|
||||
$customer_id = $this->create_customer_for_user( $user, $customer_data );
|
||||
}
|
||||
}
|
||||
|
||||
return $customer_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given user with the given WooCommerce Payments
|
||||
* customer ID.
|
||||
*
|
||||
* @param int $user_id The WordPress user ID.
|
||||
* @param string $customer_id The WooCommerce Payments customer ID.
|
||||
*/
|
||||
public function update_user_customer_id( int $user_id, string $customer_id ) {
|
||||
$global = WC_Payments::is_network_saved_cards_enabled();
|
||||
$result = update_user_option( $user_id, $this->get_customer_id_option(), $customer_id, $global );
|
||||
if ( ! $result ) {
|
||||
Logger::error( 'Failed to update customer ID for user ' . $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the WooCommerce Payments customer ID found in the user session
|
||||
* to the WordPress user as metadata.
|
||||
*
|
||||
* @param int $user_id The WordPress user ID.
|
||||
*/
|
||||
public function add_customer_id_to_user( $user_id ) {
|
||||
// Not processing a checkout, bail.
|
||||
if (
|
||||
! ( defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT ) &&
|
||||
! ( function_exists( 'wcs_is_checkout_blocks_api_request' ) && wcs_is_checkout_blocks_api_request( 'v1/checkout' ) )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the WooCommerce Payments customer ID from the user session.
|
||||
$customer_id = WC()->session ? WC()->session->get( self::CUSTOMER_ID_SESSION_KEY ) : null;
|
||||
|
||||
if ( ! $customer_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->update_user_customer_id( $user_id, $customer_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares customer data to be used on 'Pay for Order' or 'Add Payment Method' pages.
|
||||
* Customer data is retrieved from order when on Pay for Order.
|
||||
* Customer data is retrieved from customer when on 'Add Payment Method'.
|
||||
*
|
||||
* @return array|null An array with customer data or nothing.
|
||||
*/
|
||||
public function get_prepared_customer_data() {
|
||||
if ( ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return null;
|
||||
}
|
||||
|
||||
global $wp;
|
||||
$user_email = '';
|
||||
$firstname = '';
|
||||
$lastname = '';
|
||||
$billing_country = '';
|
||||
$address = null;
|
||||
|
||||
if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$order_id = absint( $wp->query_vars['order-pay'] );
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( is_a( $order, 'WC_Order' ) ) {
|
||||
$firstname = $order->get_billing_first_name();
|
||||
$lastname = $order->get_billing_last_name();
|
||||
$user_email = $order->get_billing_email();
|
||||
$billing_country = $order->get_billing_country();
|
||||
$address = [
|
||||
'city' => $order->get_billing_city(),
|
||||
'country' => $order->get_billing_country(),
|
||||
'line1' => $order->get_billing_address_1(),
|
||||
'line2' => $order->get_billing_address_2(),
|
||||
'postal_code' => $order->get_billing_postcode(),
|
||||
'state' => $order->get_billing_state(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ( is_add_payment_method_page() ) {
|
||||
$user = wp_get_current_user();
|
||||
|
||||
if ( $user->ID ) {
|
||||
$firstname = $user->user_firstname;
|
||||
$lastname = $user->user_lastname;
|
||||
$user_email = get_user_meta( $user->ID, 'billing_email', true );
|
||||
$user_email = ! empty( $user_email ) ? $user_email : $user->user_email;
|
||||
$billing_country = get_user_meta( $user->ID, 'billing_country', true );
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $firstname . ' ' . $lastname,
|
||||
'email' => $user_email,
|
||||
'billing_country' => $billing_country,
|
||||
'address' => $address,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_DB class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Wrapper class for accessing the database.
|
||||
*/
|
||||
class WC_Payments_DB {
|
||||
const META_KEY_INTENT_ID = '_intent_id';
|
||||
const META_KEY_CHARGE_ID = '_charge_id';
|
||||
|
||||
/**
|
||||
* Retrieve an order from the DB using a corresponding Stripe charge ID.
|
||||
*
|
||||
* @param string $charge_id Charge ID corresponding to an order ID.
|
||||
*
|
||||
* @return boolean|WC_Order|WC_Order_Refund
|
||||
*/
|
||||
public function order_from_charge_id( $charge_id ) {
|
||||
$order_id = $this->order_id_from_meta_key_value( self::META_KEY_CHARGE_ID, $charge_id );
|
||||
|
||||
if ( $order_id ) {
|
||||
return $this->order_from_order_id( $order_id );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve orders from the DB using a list of corresponding Stripe charge IDs.
|
||||
*
|
||||
* @param array $charge_ids List of charge IDs corresponding to an order ID.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function orders_with_charge_id_from_charge_ids( array $charge_ids ): array {
|
||||
|
||||
// The order ID is saved to DB in `WC_Payment_Gateway_WCPay::process_payment()`.
|
||||
$orders = wc_get_orders(
|
||||
[
|
||||
'limit' => count( $charge_ids ),
|
||||
'meta_key' => self::META_KEY_CHARGE_ID, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $charge_ids, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'meta_compare' => 'IN',
|
||||
]
|
||||
);
|
||||
|
||||
return array_map(
|
||||
function ( WC_Order $order ): array {
|
||||
return [
|
||||
'order' => $order,
|
||||
'charge_id' => $order->get_meta( self::META_KEY_CHARGE_ID ),
|
||||
];
|
||||
},
|
||||
$orders
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an order from the DB using a corresponding Stripe intent ID.
|
||||
*
|
||||
* @param string $intent_id Intent ID corresponding to an order ID.
|
||||
*
|
||||
* @return boolean|WC_Order|WC_Order_Refund
|
||||
*/
|
||||
public function order_from_intent_id( $intent_id ) {
|
||||
$order_id = $this->order_id_from_meta_key_value( self::META_KEY_INTENT_ID, $intent_id );
|
||||
|
||||
if ( $order_id ) {
|
||||
return $this->order_from_order_id( $order_id );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an order ID from the DB using a meta key value pair.
|
||||
*
|
||||
* @param string $meta_key Either '_intent_id' or '_charge_id'.
|
||||
* @param string $meta_value Value for the meta key.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
private function order_id_from_meta_key_value( $meta_key, $meta_value ) {
|
||||
|
||||
// Don't proceed if the meta key or value is empty.
|
||||
if ( ! $meta_key || ! $meta_value ) {
|
||||
return null;
|
||||
}
|
||||
$orders = wc_get_orders(
|
||||
[
|
||||
'limit' => 1,
|
||||
'meta_key' => $meta_key, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $meta_value, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
]
|
||||
);
|
||||
if ( $orders && ! empty( $orders ) ) {
|
||||
/**
|
||||
* As wc_get_orders may also return stdClass, Psalm infers error.
|
||||
*
|
||||
* @psalm-suppress UndefinedMethod
|
||||
*/
|
||||
return (string) $orders[0]->get_id();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an order using order ID.
|
||||
*
|
||||
* @param string $order_id WC Order Id.
|
||||
*
|
||||
* @return bool|WC_Order|WC_Order_Refund
|
||||
*/
|
||||
public function order_from_order_id( $order_id ) {
|
||||
return wc_get_order( ( $order_id ) );
|
||||
}
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Dependency_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WCPay\Database_Cache;
|
||||
|
||||
/**
|
||||
* Validates dependencies (core, plugins, versions) for WCPAY
|
||||
* Used in the plugin main class for validation.
|
||||
*/
|
||||
class WC_Payments_Dependency_Service {
|
||||
|
||||
const WOOCORE_NOT_FOUND = 'woocore_disabled';
|
||||
const WOOCORE_INCOMPATIBLE = 'woocore_outdated';
|
||||
const WOOADMIN_NOT_FOUND = 'wc_admin_not_found';
|
||||
const WOOADMIN_INCOMPATIBLE = 'wc_admin_outdated';
|
||||
const WP_INCOMPATIBLE = 'wp_outdated';
|
||||
const DEV_ASSETS_NOT_BUILT = 'dev_assets_not_built';
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_filter( 'admin_notices', [ $this, 'display_admin_notices' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all the dependencies needed to run WooPayments are present
|
||||
*
|
||||
* @return bool True if all required dependencies are met.
|
||||
*/
|
||||
public function has_valid_dependencies() {
|
||||
|
||||
if ( defined( 'WCPAY_TEST_ENV' ) && WCPAY_TEST_ENV ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return empty( $this->get_invalid_dependencies( true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin notices for unmet dependencies. Called on the admin_notices hook.
|
||||
*
|
||||
* @return null.
|
||||
*/
|
||||
public function display_admin_notices() {
|
||||
|
||||
// Do not show alerts while installing plugins.
|
||||
if ( self::is_at_plugin_install_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show a message when assets are not built in a dev build.
|
||||
if ( ! $this->are_assets_built() ) {
|
||||
WC_Payments::display_admin_error( $this->get_notice_for_invalid_dependency( self::DEV_ASSETS_NOT_BUILT ) );
|
||||
}
|
||||
|
||||
$invalid_dependencies = $this->get_invalid_dependencies();
|
||||
|
||||
if ( ! empty( $invalid_dependencies ) ) {
|
||||
WC_Payments::display_admin_error( $this->get_notice_for_invalid_dependency( $invalid_dependencies[0] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of invalid dependencies
|
||||
*
|
||||
* @param bool $check_account_connection - if should bypass dependency version validation when an account is connected.
|
||||
*
|
||||
* @return array of invalid dependencies as string constants.
|
||||
*/
|
||||
public function get_invalid_dependencies( bool $check_account_connection = false ) {
|
||||
|
||||
$invalid_dependencies = [];
|
||||
|
||||
// Either ignore the account connection check or check if there's a cached account connection.
|
||||
$ignore_when_account_is_connected = $check_account_connection && self::has_cached_account_connection();
|
||||
|
||||
if ( ! $this->is_woo_core_active() ) {
|
||||
$invalid_dependencies[] = self::WOOCORE_NOT_FOUND;
|
||||
}
|
||||
|
||||
if ( ! $ignore_when_account_is_connected && ! $this->is_woo_core_version_compatible() ) {
|
||||
$invalid_dependencies[] = self::WOOCORE_INCOMPATIBLE;
|
||||
}
|
||||
|
||||
if ( ! $this->is_wc_admin_enabled() ) {
|
||||
$invalid_dependencies[] = self::WOOADMIN_NOT_FOUND;
|
||||
}
|
||||
|
||||
if ( ! $ignore_when_account_is_connected && ! $this->is_wc_admin_version_compatible() ) {
|
||||
$invalid_dependencies[] = self::WOOADMIN_INCOMPATIBLE;
|
||||
}
|
||||
|
||||
if ( ! $ignore_when_account_is_connected && ! $this->is_wp_version_compatible() ) {
|
||||
$invalid_dependencies[] = self::WP_INCOMPATIBLE;
|
||||
}
|
||||
|
||||
return $invalid_dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if WooCommerce is installed and activated.
|
||||
*
|
||||
* @return bool True if WooCommerce is installed and activated.
|
||||
*/
|
||||
public function is_woo_core_active() {
|
||||
// Check if WooCommerce is installed and active.
|
||||
return class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the version of WooCommerce is compatible with WooPayments.
|
||||
*
|
||||
* @return bool True if WooCommerce version is greater than or equal the minimum accepted
|
||||
*/
|
||||
public function is_woo_core_version_compatible() {
|
||||
|
||||
$plugin_headers = WC_Payments::get_plugin_headers();
|
||||
$wc_version = $plugin_headers['WCRequires'];
|
||||
|
||||
// Check if the version of WooCommerce is compatible with WooPayments.
|
||||
return ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, $wc_version, '>=' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the WooCommerce version has WooCommerce Admin bundled (WC 4.0+)
|
||||
* but it's disabled using a filter.
|
||||
*
|
||||
* @return bool True if WC Admin is found
|
||||
*/
|
||||
public function is_wc_admin_enabled() {
|
||||
|
||||
// Check if the current WooCommerce version has WooCommerce Admin bundled (WC 4.0+) but it's disabled using a filter.
|
||||
if ( ! defined( 'WC_ADMIN_VERSION_NUMBER' ) || apply_filters( 'woocommerce_admin_disabled', false ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the version of WC Admin is compatible with WooPayments.
|
||||
*
|
||||
* @return bool True if WC Admin version is greater than or equal the minimum accepted
|
||||
*/
|
||||
public function is_wc_admin_version_compatible() {
|
||||
|
||||
// Check if the version of WooCommerce Admin is compatible with WooPayments.
|
||||
return ( defined( 'WC_ADMIN_VERSION_NUMBER' ) && version_compare( WC_ADMIN_VERSION_NUMBER, WCPAY_MIN_WC_ADMIN_VERSION, '>=' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the version of WordPress is compatible with WooPayments.
|
||||
*
|
||||
* @return bool True if WordPress version is greater than or equal the minimum accepted
|
||||
*/
|
||||
public function is_wp_version_compatible() {
|
||||
|
||||
$plugin_headers = WC_Payments::get_plugin_headers();
|
||||
$wp_version = $plugin_headers['RequiresWP'];
|
||||
|
||||
return version_compare( get_bloginfo( 'version' ), $wp_version, '>=' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks some of the asset files to confirm scripts and styles have been correctly built.
|
||||
*
|
||||
* @return bool TRUE if assets have been built or FALSE otherwise.
|
||||
*/
|
||||
public function are_assets_built() {
|
||||
return ( file_exists( WCPAY_ABSPATH . 'dist/index.js' ) && file_exists( WCPAY_ABSPATH . 'dist/index.css' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error constant of an invalid dependency, and transforms it into HTML to be used in an Admin Notice.
|
||||
*
|
||||
* @param string $code - invalid dependency constant.
|
||||
*
|
||||
* @return string HTML to render admin notice for the unmet dependency.
|
||||
*/
|
||||
private function get_notice_for_invalid_dependency( $code ) {
|
||||
|
||||
$plugin_headers = WC_Payments::get_plugin_headers();
|
||||
$wp_version = $plugin_headers['RequiresWP'];
|
||||
$wc_version = $plugin_headers['WCRequires'];
|
||||
|
||||
$error_message = '';
|
||||
|
||||
switch ( $code ) {
|
||||
case self::WOOCORE_NOT_FOUND:
|
||||
$error_message = WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %1$s: WooPayments, %2$s: WooCommerce */
|
||||
__( '%1$s requires <a>%2$s</a> to be installed and active.', 'woocommerce-payments' ),
|
||||
'WooPayments',
|
||||
'WooCommerce'
|
||||
),
|
||||
[ 'a' => '<a href="https://wordpress.org/plugins/woocommerce">' ]
|
||||
);
|
||||
|
||||
if ( current_user_can( 'install_plugins' ) ) {
|
||||
if ( is_wp_error( validate_plugin( 'woocommerce/woocommerce.php' ) ) ) {
|
||||
// WooCommerce is not installed.
|
||||
$activate_url = wp_nonce_url( admin_url( 'update.php?action=install-plugin&plugin=woocommerce' ), 'install-plugin_woocommerce' );
|
||||
$activate_text = __( 'Install WooCommerce', 'woocommerce-payments' );
|
||||
} else {
|
||||
// WooCommerce is installed, so it just needs to be enabled.
|
||||
$activate_url = wp_nonce_url( admin_url( 'plugins.php?action=activate&plugin=woocommerce/woocommerce.php' ), 'activate-plugin_woocommerce/woocommerce.php' );
|
||||
$activate_text = __( 'Activate WooCommerce', 'woocommerce-payments' );
|
||||
}
|
||||
$error_message .= ' <a href="' . $activate_url . '">' . $activate_text . '</a>';
|
||||
}
|
||||
|
||||
break;
|
||||
case self::WOOCORE_INCOMPATIBLE:
|
||||
$error_message = WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %1: WooPayments, %2: current WooCommerce Payment version, %3: WooCommerce, %4: required WC version number, %5: currently installed WC version number */
|
||||
__( '%1$s %2$s requires <strong>%3$s %4$s</strong> or greater to be installed (you are using %5$s). ', 'woocommerce-payments' ),
|
||||
'WooPayments',
|
||||
WCPAY_VERSION_NUMBER,
|
||||
'WooCommerce',
|
||||
$wc_version,
|
||||
WC_VERSION
|
||||
),
|
||||
[ 'strong' => '<strong>' ]
|
||||
);
|
||||
|
||||
if ( current_user_can( 'update_plugins' ) ) {
|
||||
// Take the user to the "plugins" screen instead of trying to update WooCommerce inline. WooCommerce adds important information
|
||||
// on its plugin row regarding the currently installed extensions and their compatibility with the latest WC version.
|
||||
$error_message .= '<br/>' . WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %1$s: WooCommerce, %2$s: WooPayments, a1: link to the Plugins page, a2: link to the page having all previous versions */
|
||||
__( '<a1>Update %1$s</a1> <strong>(recommended)</strong> or manually re-install <a2>a previous version</a2> of %2$s.', 'woocommerce-payments' ),
|
||||
'WooCommerce',
|
||||
'WooPayments'
|
||||
),
|
||||
[
|
||||
|
||||
'a1' => '<a href="' . admin_url( 'plugins.php' ) . '">',
|
||||
'strong' => '<strong>',
|
||||
'a2' => '<a href="https://wordpress.org/plugins/woocommerce-payments/advanced/#download-previous-link" target="_blank">',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case self::WOOADMIN_NOT_FOUND:
|
||||
$error_message = WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %1$s: WooPayments, %2$s: WooCommerce Admin */
|
||||
__( '%1$s requires %2$s to be enabled. Please remove the <code>woocommerce_admin_disabled</code> filter to use %1$s.', 'woocommerce-payments' ),
|
||||
'WooPayments',
|
||||
'WooCommerce Admin'
|
||||
),
|
||||
[ 'code' => '<code>' ]
|
||||
);
|
||||
|
||||
break;
|
||||
case self::WOOADMIN_INCOMPATIBLE:
|
||||
$error_message = WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %1: WooPayments, %2: WooCommerce Admin, %3: required WC-Admin version number, %4: currently installed WC-Admin version number */
|
||||
__( '%1$s requires <strong>%2$s %3$s</strong> or greater to be installed (you are using %4$s).', 'woocommerce-payments' ),
|
||||
'WooPayments',
|
||||
'WooCommerce Admin',
|
||||
WCPAY_MIN_WC_ADMIN_VERSION,
|
||||
WC_ADMIN_VERSION_NUMBER
|
||||
),
|
||||
[ 'strong' => '<strong>' ]
|
||||
);
|
||||
|
||||
// Let's assume for now that any WC-Admin version bundled with WooCommerce will meet our minimum requirements.
|
||||
$error_message .= ' ' . __( 'There is a newer version of WooCommerce Admin bundled with WooCommerce.', 'woocommerce-payments' );
|
||||
|
||||
if ( current_user_can( 'deactivate_plugins' ) ) {
|
||||
$deactivate_url = wp_nonce_url( admin_url( 'plugins.php?action=deactivate&plugin=woocommerce-admin/woocommerce-admin.php' ), 'deactivate-plugin_woocommerce-admin/woocommerce-admin.php' );
|
||||
$error_message .= ' <a href="' . $deactivate_url . '">' . __( 'Use the bundled version of WooCommerce Admin', 'woocommerce-payments' ) . '</a>';
|
||||
}
|
||||
|
||||
break;
|
||||
case self::WP_INCOMPATIBLE:
|
||||
$error_message = WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %1: WooPayments, %2: required WP version number, %3: currently installed WP version number */
|
||||
__( '%1$s requires <strong>WordPress %2$s</strong> or greater (you are using %3$s).', 'woocommerce-payments' ),
|
||||
'WooPayments',
|
||||
$wp_version,
|
||||
get_bloginfo( 'version' )
|
||||
),
|
||||
[ 'strong' => '<strong>' ]
|
||||
);
|
||||
if ( current_user_can( 'update_core' ) ) {
|
||||
$error_message .= ' <a href="' . admin_url( 'update-core.php' ) . '">' . __( 'Update WordPress', 'woocommerce-payments' ) . '</a>';
|
||||
}
|
||||
break;
|
||||
case self::DEV_ASSETS_NOT_BUILT:
|
||||
$error_message = WC_Payments_Utils::esc_interpolated_html(
|
||||
sprintf(
|
||||
/* translators: %s: WooPayments */
|
||||
__(
|
||||
'You have installed a development version of %s which requires files to be built. From the plugin directory, run <code>npm run build:client</code> to build and minify assets. Alternatively, you can download a pre-built version of the plugin from the <a1>WordPress.org repository</a1> or by visiting the <a2>releases page in the GitHub repository</a2>.',
|
||||
'woocommerce-payments'
|
||||
),
|
||||
'WooPayments'
|
||||
),
|
||||
[
|
||||
'code' => '<code>',
|
||||
'a1' => '<a href="https://wordpress.org/plugins/woocommerce-payments/">',
|
||||
'a2' => '<a href="https://github.com/automattic/woocommerce-payments/releases/">',
|
||||
]
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return $error_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current page is plugin installation process page.
|
||||
*
|
||||
* @return bool True when installing plugin.
|
||||
*/
|
||||
private static function is_at_plugin_install_page() {
|
||||
$cur_screen = get_current_screen();
|
||||
return $cur_screen && 'update' === $cur_screen->id && 'plugins' === $cur_screen->parent_base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current WCPay Account has cache data.
|
||||
*
|
||||
* @return bool True if the cache data exists in wp_options.
|
||||
*/
|
||||
private static function has_cached_account_connection(): bool {
|
||||
$account_data = get_option( Database_Cache::ACCOUNT_KEY );
|
||||
return isset( $account_data['data'] ) && is_array( $account_data['data'] ) && ! empty( $account_data['data'] );
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Explicit_Price_Formatter
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for displaying the explicit prices on total amounts.
|
||||
*
|
||||
* Also used for consistent rendering of price values with currency (get_explicit_price_with_currency()).
|
||||
*/
|
||||
class WC_Payments_Explicit_Price_Formatter {
|
||||
|
||||
/**
|
||||
* The Multi-Currency instance for checking the number of enabled currencies
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
private static $multi_currency_instance = null;
|
||||
|
||||
/**
|
||||
* Inits the formatter, registering the necessary hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
add_filter( 'woocommerce_cart_total', [ __CLASS__, 'get_explicit_price' ], 100 );
|
||||
add_filter( 'woocommerce_get_formatted_order_total', [ __CLASS__, 'get_explicit_price' ], 100, 2 );
|
||||
add_action( 'woocommerce_admin_order_totals_after_tax', [ __CLASS__, 'register_formatted_woocommerce_price_filter' ] );
|
||||
add_action( 'woocommerce_admin_order_totals_after_total', [ __CLASS__, 'unregister_formatted_woocommerce_price_filter' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the MultiCurrency instance that the class uses.
|
||||
* Mostly for testing purposes.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency MultCurrency class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_multi_currency_instance( MultiCurrency $multi_currency ) {
|
||||
self::$multi_currency_instance = $multi_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the method should output explicit price
|
||||
*
|
||||
* @return bool Whether if it should return explicit price or not
|
||||
*/
|
||||
public static function should_output_explicit_price() {
|
||||
// If customer Multi-Currency is disabled, don't use explicit currencies.
|
||||
// Because it'll have only the store currency active, same as count == 1.
|
||||
if ( ! WC_Payments_Features::is_customer_multi_currency_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the MultiCurrency instance hasn't been defined yet, fetch the instance.
|
||||
if ( null === self::$multi_currency_instance ) {
|
||||
self::$multi_currency_instance = WC_Payments_Multi_Currency();
|
||||
}
|
||||
|
||||
// If the instance isn't initialized yet, skip the checks.
|
||||
if ( ! self::$multi_currency_instance->is_initialized() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no additional currencies are enabled, skip it.
|
||||
if ( ! self::$multi_currency_instance->has_additional_currencies_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the get_explicit_price filter for the order details screen.
|
||||
*
|
||||
* There are no hooks that enable us to filter the output on the order details screen.
|
||||
* So, we need to add a filter to formatted_woocommerce_price. We use specific actions
|
||||
* to register and unregister the filter, so that only the appropriate prices are affected.
|
||||
*/
|
||||
public static function register_formatted_woocommerce_price_filter() {
|
||||
add_filter( 'wc_price_args', [ __CLASS__, 'get_explicit_price_args' ], 100 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the get_explicit_price filter for the order details screen.
|
||||
*
|
||||
* There are no hooks that enable us to filter the output on the order details screen.
|
||||
* So, we need to add a filter to formatted_woocommerce_price. We use specific actions
|
||||
* to register and unregister the filter, so that only the appropriate prices are affected.
|
||||
*/
|
||||
public static function unregister_formatted_woocommerce_price_filter() {
|
||||
remove_filter( 'wc_price_args', [ __CLASS__, 'get_explicit_price_args' ], 100 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the price suffixed with the appropriate currency code, if not already.
|
||||
*
|
||||
* @param string $price The price.
|
||||
* @param WC_Abstract_Order|null $order The order.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_explicit_price( string $price, ?WC_Abstract_Order $order = null ) {
|
||||
if ( null === $order ) {
|
||||
$currency_code = get_woocommerce_currency();
|
||||
} else {
|
||||
$currency_code = $order->get_currency();
|
||||
}
|
||||
|
||||
return static::get_explicit_price_with_currency( $price, $currency_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted price string suffixed with the appropriate currency code (if necessary).
|
||||
*
|
||||
* In multi-currency stores, order and price values are rendered with currency suffix.
|
||||
* This method only renders the currency suffix if appropriate (see `should_output_explicit_price`).
|
||||
*
|
||||
* @param string $price A price value (as a string).
|
||||
* @param ?string $currency_code Currency of the price.
|
||||
*
|
||||
* @return string Price value with currency code suffix if necessary.
|
||||
*/
|
||||
public static function get_explicit_price_with_currency( string $price, ?string $currency_code ) {
|
||||
if ( false === static::should_output_explicit_price() ) {
|
||||
return $price;
|
||||
}
|
||||
if ( empty( $currency_code ) ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
$price_to_check = html_entity_decode( wp_strip_all_tags( $price ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
|
||||
if ( false === strpos( $price_to_check, trim( $currency_code ) ) ) {
|
||||
return $price . ' ' . $currency_code;
|
||||
}
|
||||
|
||||
return $price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters the price formatting arguments to include explicit format
|
||||
*
|
||||
* @param array $args Price formatting args passed through `wc_price_args` filter.
|
||||
*
|
||||
* @return array The modified arguments
|
||||
*/
|
||||
public static function get_explicit_price_args( $args ) {
|
||||
if ( false === static::should_output_explicit_price() ) {
|
||||
return $args;
|
||||
}
|
||||
if ( false === strpos( $args['price_format'], $args['currency'] ) ) {
|
||||
$args['price_format'] = sprintf( '%s %s', $args['price_format'], $args['currency'] );
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Features
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Constants\Country_Code;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* WC Payments Features class
|
||||
*/
|
||||
class WC_Payments_Features {
|
||||
/**
|
||||
* If you need to remove or deprecate a flag:
|
||||
* - Please update the `Erase_Deprecated_Flags_And_Options` migration with:
|
||||
* - The next version of WooPayments.
|
||||
* - The flag to be deleted.
|
||||
*/
|
||||
const WCPAY_SUBSCRIPTIONS_FLAG_NAME = '_wcpay_feature_subscriptions';
|
||||
const STRIPE_BILLING_FLAG_NAME = '_wcpay_feature_stripe_billing';
|
||||
const WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_express_checkout';
|
||||
const WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME = '_wcpay_feature_woopay_first_party_auth';
|
||||
const WOOPAY_DIRECT_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_direct_checkout';
|
||||
const AUTH_AND_CAPTURE_FLAG_NAME = '_wcpay_feature_auth_and_capture';
|
||||
const DISPUTE_ISSUER_EVIDENCE = '_wcpay_feature_dispute_issuer_evidence';
|
||||
const TOKENIZED_CART_ECE_FLAG_NAME = '_wcpay_feature_tokenized_cart_ece';
|
||||
const PAYMENT_OVERVIEW_WIDGET_FLAG_NAME = '_wcpay_feature_payment_overview_widget';
|
||||
const WOOPAY_GLOBAL_THEME_SUPPORT_FLAG_NAME = '_wcpay_feature_woopay_global_theme_support';
|
||||
|
||||
/**
|
||||
* Indicates whether card payments are enabled for this (Stripe) account.
|
||||
*
|
||||
* @return bool True if account can accept card payments, false otherwise.
|
||||
*/
|
||||
public static function are_payments_enabled() {
|
||||
$account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
|
||||
|
||||
return is_array( $account ) && ( $account['payments_enabled'] ?? false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the "tokenized cart" feature for PRBs is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_tokenized_cart_ece_enabled(): bool {
|
||||
return '1' === get_option( self::TOKENIZED_CART_ECE_FLAG_NAME, '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if WooPay is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_woopay_enabled() {
|
||||
$is_woopay_eligible = self::is_woopay_eligible(); // Feature flag.
|
||||
$is_woopay_enabled = 'yes' === WC_Payments::get_gateway()->get_option( 'platform_checkout' );
|
||||
$is_woopay_express_button_enabled = self::is_woopay_express_checkout_enabled();
|
||||
|
||||
return $is_woopay_eligible && $is_woopay_enabled && $is_woopay_express_button_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the customer Multi-Currency feature is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_customer_multi_currency_enabled() {
|
||||
return '1' === get_option( '_wcpay_feature_customer_multi_currency', '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether WCPay Subscriptions is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_wcpay_subscriptions_enabled() {
|
||||
// After completing the WooCommerce onboarding, check if the merchant has chosen Subscription product types and enable the feature flag.
|
||||
if ( (bool) get_option( 'wcpay_check_subscriptions_eligibility_after_onboarding', false ) ) {
|
||||
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.9.0', '<' ) ) {
|
||||
self::maybe_enable_wcpay_subscriptions_after_onboarding( [], get_option( 'woocommerce_onboarding_profile', [] ) );
|
||||
}
|
||||
|
||||
delete_option( 'wcpay_check_subscriptions_eligibility_after_onboarding' );
|
||||
}
|
||||
|
||||
return apply_filters( 'wcpay_is_wcpay_subscriptions_enabled', '1' === get_option( self::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the store is eligible to use WCPay Subscriptions (the free subscriptions bundled in WooPayments)
|
||||
*
|
||||
* Stores are eligible for the WCPay Subscriptions feature if:
|
||||
* 1. The store has existing WCPay Subscriptions, or
|
||||
* 2. The store has Stripe Billing product metadata on at least 1 product subscription product.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_wcpay_subscriptions_eligible() {
|
||||
/**
|
||||
* Check if they have at least 1 WCPay Subscription.
|
||||
*
|
||||
* Note: this is only possible if WCPay Subscriptions is enabled, otherwise the wcs_get_subscriptions function wouldn't exist.
|
||||
*/
|
||||
if ( function_exists( 'wcs_get_subscriptions' ) ) {
|
||||
$wcpay_subscriptions = wcs_get_subscriptions(
|
||||
[
|
||||
'subscriptions_per_page' => 1,
|
||||
'subscription_status' => 'any',
|
||||
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
[
|
||||
'key' => '_wcpay_subscription_id',
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if ( ( is_countable( $wcpay_subscriptions ) ? count( $wcpay_subscriptions ) : 0 ) > 0 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if they have at least 1 Stripe Billing enabled product.
|
||||
*/
|
||||
$stripe_billing_meta_query_handler = function ( $query, $query_vars ) {
|
||||
if ( ! empty( $query_vars['stripe_billing_product'] ) ) {
|
||||
$query['meta_query'][] = [
|
||||
'key' => '_wcpay_product_hash',
|
||||
'compare' => 'EXISTS',
|
||||
];
|
||||
}
|
||||
|
||||
return $query;
|
||||
};
|
||||
|
||||
add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 );
|
||||
|
||||
$subscription_products = wc_get_products(
|
||||
[
|
||||
'limit' => 1,
|
||||
'type' => [ 'subscription', 'variable-subscription' ],
|
||||
'status' => 'publish',
|
||||
'return' => 'ids',
|
||||
'stripe_billing_product' => 'true',
|
||||
]
|
||||
);
|
||||
|
||||
remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 );
|
||||
|
||||
if ( ( is_countable( $subscription_products ) ? count( $subscription_products ) : 0 ) > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the merchant has chosen Subscription product types during onboarding
|
||||
* WooCommerce and is elible for WCPay Subscriptions, if so, enables the feature flag.
|
||||
*
|
||||
* @since 6.2.0
|
||||
*
|
||||
* @param array $onboarding_data Onboarding data.
|
||||
* @param array $updated Updated onboarding settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_enable_wcpay_subscriptions_after_onboarding( $onboarding_data, $updated ) {
|
||||
if ( empty( $updated['product_types'] ) || ! is_array( $updated['product_types'] ) || ! in_array( 'subscriptions', $updated['product_types'], true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! self::is_wcpay_subscriptions_eligible() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( self::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether woopay is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_woopay_eligible() {
|
||||
// Checks for the dependency on Store API AbstractCartRoute.
|
||||
if ( ! class_exists( 'Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// read directly from cache, ignore cache expiration check.
|
||||
$account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
|
||||
|
||||
$is_account_rejected = WC_Payments::get_account_service()->is_account_rejected();
|
||||
|
||||
$is_account_under_review = WC_Payments::get_account_service()->is_account_under_review();
|
||||
|
||||
return is_array( $account )
|
||||
&& ( $account['platform_checkout_eligible'] ?? false )
|
||||
&& ! $is_account_rejected
|
||||
&& ! $is_account_under_review;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether documents section is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_documents_section_enabled() {
|
||||
$account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY );
|
||||
$is_documents_enabled = is_array( $account ) && ( $account['is_documents_enabled'] ?? false );
|
||||
return '1' === get_option( '_wcpay_feature_documents', $is_documents_enabled ? '1' : '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether WooPay Express Checkout is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_woopay_express_checkout_enabled() {
|
||||
// Confirm woopay eligibility as well.
|
||||
return '1' === get_option( self::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ) && self::is_woopay_eligible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether WooPay First Party Auth is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_woopay_first_party_auth_enabled() {
|
||||
return '1' === get_option( self::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' ) && self::is_woopay_express_checkout_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether Payment Overview Widget is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_payment_overview_widget_ui_enabled(): bool {
|
||||
return '1' === get_option( self::PAYMENT_OVERVIEW_WIDGET_FLAG_NAME, '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether WooPay Direct Checkout is enabled.
|
||||
*
|
||||
* @return bool True if Direct Checkout is enabled, false otherwise.
|
||||
*/
|
||||
public static function is_woopay_direct_checkout_enabled() {
|
||||
$account_cache = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
|
||||
$is_direct_checkout_eligible = is_array( $account_cache ) && ( $account_cache['platform_direct_checkout_eligible'] ?? false );
|
||||
$is_direct_checkout_flag_enabled = '1' === get_option( self::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' );
|
||||
|
||||
return $is_direct_checkout_eligible && $is_direct_checkout_flag_enabled && self::is_woopayments_gateway_enabled() && self::is_woopay_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether WooPay global theme support is eligible.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_woopay_global_theme_support_eligible() {
|
||||
$account_cache = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true );
|
||||
|
||||
return is_array( $account_cache ) && ( $account_cache['platform_global_theme_support_enabled'] ?? false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether Auth & Capture (uncaptured transactions tab, capture from payment details page) is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_auth_and_capture_enabled() {
|
||||
return '1' === get_option( self::AUTH_AND_CAPTURE_FLAG_NAME, '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Fraud and Risk Tools feature flag is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_frt_review_feature_active(): bool {
|
||||
return '1' === get_option( 'wcpay_frt_review_feature_active', '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Fraud and Risk Tools welcome tour was dismissed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_fraud_protection_welcome_tour_dismissed(): bool {
|
||||
return '1' === get_option( 'wcpay_fraud_protection_welcome_tour_dismissed', '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Stripe Billing feature is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_stripe_billing_enabled(): bool {
|
||||
return '1' === get_option( self::STRIPE_BILLING_FLAG_NAME, '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the site is eligible for Stripe Billing.
|
||||
*
|
||||
* Only US merchants are eligible for Stripe Billing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_stripe_billing_eligible() {
|
||||
if ( ! function_exists( 'wc_get_base_location' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$store_base_location = wc_get_base_location();
|
||||
return ! empty( $store_base_location['country'] ) && Country_Code::UNITED_STATES === $store_base_location['country'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the merchant is using WCPay Subscription or opted into Stripe Billing.
|
||||
*
|
||||
* Note: Stripe Billing is only used when the merchant is using WooCommerce Subscriptions and turned it on or is still using WCPay Subscriptions.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_use_stripe_billing() {
|
||||
// We intentionally check for the existence of the 'WC_Subscriptions' class here as we want to confirm the Plugin is active.
|
||||
if ( self::is_wcpay_subscriptions_enabled() && ! class_exists( 'WC_Subscriptions' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( self::is_stripe_billing_enabled() && class_exists( 'WC_Subscriptions' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether Dispute issuer evidence feature should be enabled. Disabled by default.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_dispute_issuer_evidence_enabled(): bool {
|
||||
return '1' === get_option( self::DISPUTE_ISSUER_EVIDENCE, '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the next deposit notice on the deposits list screen has been dismissed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_next_deposit_notice_dismissed(): bool {
|
||||
return '1' === get_option( 'wcpay_next_deposit_notice_dismissed', '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature flags as an array suitable for display on the front-end.
|
||||
*
|
||||
* @return bool[]
|
||||
*/
|
||||
public static function to_array() {
|
||||
return array_filter(
|
||||
[
|
||||
'multiCurrency' => self::is_customer_multi_currency_enabled(),
|
||||
'woopay' => self::is_woopay_eligible(),
|
||||
'documents' => self::is_documents_section_enabled(),
|
||||
'woopayExpressCheckout' => self::is_woopay_express_checkout_enabled(),
|
||||
'isAuthAndCaptureEnabled' => self::is_auth_and_capture_enabled(),
|
||||
'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(),
|
||||
'isPaymentOverviewWidgetEnabled' => self::is_payment_overview_widget_ui_enabled(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if WooCommerce Payments gateway is enabled.
|
||||
*
|
||||
* @return bool True if WooCommerce Payments gateway is enabled, false otherwise.
|
||||
*/
|
||||
private static function is_woopayments_gateway_enabled() {
|
||||
$woopayments_settings = get_option( 'woocommerce_woocommerce_payments_settings' );
|
||||
$woopayments_enabled_setting = $woopayments_settings['enabled'] ?? 'no';
|
||||
|
||||
return 'yes' === $woopayments_enabled_setting;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_File_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class which handles files.
|
||||
*/
|
||||
class WC_Payments_File_Service {
|
||||
|
||||
|
||||
const FILE_PURPOSE_PUBLIC = [
|
||||
'business_logo',
|
||||
'business_icon',
|
||||
];
|
||||
|
||||
const CACHE_KEY_PREFIX_PURPOSE = 'file_purpoose_';
|
||||
const CACHE_PERIOD = 86400; // 24 h
|
||||
|
||||
|
||||
/**
|
||||
* Check if a file purpose is public or needs permissions.
|
||||
*
|
||||
* @param string $purpose - file purpose.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_file_public( string $purpose ): bool {
|
||||
return in_array( $purpose, static::FILE_PURPOSE_PUBLIC, true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Fraud_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use WCPay\Database_Cache;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
use WCPay\Logger;
|
||||
|
||||
/**
|
||||
* Class which includes all the fraud-specific logic.
|
||||
*/
|
||||
class WC_Payments_Fraud_Service {
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* WC_Payments_Account instance to get information about the account
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC_Payments_Customer instance for working with customer information
|
||||
*
|
||||
* @var WC_Payments_Customer_Service
|
||||
*/
|
||||
private $customer_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Session_Service instance for working with session information
|
||||
*
|
||||
* @var WC_Payments_Session_Service
|
||||
*/
|
||||
private $session_service;
|
||||
|
||||
/**
|
||||
* Cache util for managing the database cached data.
|
||||
*
|
||||
* @var Database_Cache
|
||||
*/
|
||||
private $database_cache;
|
||||
|
||||
/**
|
||||
* Constructor for WC_Payments_Fraud_Service.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client.
|
||||
* @param WC_Payments_Customer_Service $customer_service - Customer class instance.
|
||||
* @param WC_Payments_Account $account - Account class instance.
|
||||
* @param WC_Payments_Session_Service $session_service - Session Service class instance.
|
||||
* @param Database_Cache $database_cache - Database cache instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $payments_api_client,
|
||||
WC_Payments_Customer_Service $customer_service,
|
||||
WC_Payments_Account $account,
|
||||
WC_Payments_Session_Service $session_service,
|
||||
Database_Cache $database_cache
|
||||
) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
$this->customer_service = $customer_service;
|
||||
$this->account = $account;
|
||||
$this->session_service = $session_service;
|
||||
$this->database_cache = $database_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'init', [ $this, 'link_session_if_user_just_logged_in' ] );
|
||||
add_action( 'admin_print_footer_scripts', [ $this, 'add_sift_js_tracker_in_admin' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the various anti-fraud services that must be included on every WCPay-related page.
|
||||
*
|
||||
* @return array Assoc array. Each key is the slug of a fraud service that must be incorporated to every page.
|
||||
* The value is a service-specific config for it.
|
||||
*/
|
||||
public function get_fraud_services_config(): array {
|
||||
$raw_config = null;
|
||||
|
||||
// First, try to get the config from the account data.
|
||||
// This config takes precedence since it can be merchant-specific.
|
||||
// We expect this entry to contain everything needed for the fraud services to work.
|
||||
$account = $this->account->get_cached_account_data();
|
||||
if ( ! empty( $account ) && isset( $account['fraud_services'] ) ) {
|
||||
$raw_config = $account['fraud_services'];
|
||||
}
|
||||
|
||||
// If the fraud services config is not available in the account data, try to get it from the server.
|
||||
// This is a public, merchant-agnostic config.
|
||||
// We expect the server to provide everything needed for the fraud services to work.
|
||||
// If we've been given an empty array, we respect that; so no empty checks.
|
||||
if ( is_null( $raw_config ) ) {
|
||||
$raw_config = $this->get_cached_fraud_services();
|
||||
}
|
||||
|
||||
if ( is_null( $raw_config ) ) {
|
||||
// This was the default before adding new anti-fraud providers, preserve backwards-compatibility.
|
||||
$raw_config = [ 'stripe' => [] ];
|
||||
}
|
||||
|
||||
$services_config = [];
|
||||
foreach ( $raw_config as $service_id => $config ) {
|
||||
if ( ! is_array( $config ) ) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
// Apply our internal logic before allowing others to have a say through filters.
|
||||
$config = $this->prepare_fraud_config( $config, $service_id );
|
||||
|
||||
$services_config[ $service_id ] = apply_filters( 'wcpay_prepare_fraud_config', $config, $service_id );
|
||||
}
|
||||
|
||||
return $services_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has just logged in,
|
||||
* and sends that information to the server to link the current browser session with the user.
|
||||
*
|
||||
* Called after the WooCommerce session has been initialized.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception In case the main gateway class has not been initialized yet.
|
||||
* This means that the method is called before the `init` hook.
|
||||
*/
|
||||
public function link_session_if_user_just_logged_in() {
|
||||
$wpcom_blog_id = $this->payments_api_client->get_blog_id();
|
||||
if ( ! $wpcom_blog_id ) {
|
||||
// Don't do anything if Jetpack hasn't been connected yet.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->session_service->user_just_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fraud_config = $this->get_fraud_services_config();
|
||||
if ( ! isset( $fraud_config['sift'] ) ) {
|
||||
// If Sift isn't enabled, we don't need to link the session.
|
||||
return;
|
||||
}
|
||||
|
||||
// The session changed during the current page load, for example if the user just logged in.
|
||||
// In this case, send the old session's customer ID alongside the new user_id so SIFT can link them.
|
||||
$customer_id = $this->customer_service->get_customer_id_by_user_id( get_current_user_id() );
|
||||
if ( ! isset( $customer_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->session_service->link_current_session_to_customer( $customer_id );
|
||||
} catch ( API_Exception $e ) {
|
||||
Logger::log( '[Tracking] Error when linking session with user: ' . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Sift JS page tracker in the WP admin area, if needed.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception In case the main gateway class has not been initialized yet.
|
||||
* This means that the method is called before the `init` hook.
|
||||
*/
|
||||
public function add_sift_js_tracker_in_admin() {
|
||||
// If the current page is a WooPayments dashboard page bail as there's separate logic
|
||||
// that will include the Sift JS tracker on its own.
|
||||
if ( isset( $_GET['path'] ) && strpos( $_GET['path'], '/payments/' ) === 0 ) { // phpcs:ignore WordPress.Security
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail if this is not a WooCommerce admin page.
|
||||
if ( is_callable( '\Automattic\WooCommerce\Admin\PageController::is_admin_or_embed_page' )
|
||||
&& ! \Automattic\WooCommerce\Admin\PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail if Sift is not enabled (either globally or for the current account).
|
||||
$fraud_services_config = $this->get_fraud_services_config();
|
||||
if ( ! isset( $fraud_services_config['sift'] ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
var src = 'https://cdn.sift.com/s.js';
|
||||
|
||||
var _sift = ( window._sift = window._sift || [] );
|
||||
_sift.push( [ '_setAccount', '<?php echo esc_attr( $fraud_services_config['sift']['beacon_key'] ); ?>' ] );
|
||||
_sift.push( [ '_setUserId', '<?php echo esc_attr( $fraud_services_config['sift']['user_id'] ); ?>' ] );
|
||||
_sift.push( [ '_setSessionId', '<?php echo esc_attr( $fraud_services_config['sift']['session_id'] ); ?>' ] );
|
||||
_sift.push( [ '_trackPageview' ] );
|
||||
|
||||
if ( ! document.querySelector( '[src="' + src + '"]' ) ) {
|
||||
var script = document.createElement( 'script' );
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
document.body.appendChild( script );
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cached fraud services config can be used.
|
||||
*
|
||||
* @param bool|string|array $fraud_services_config The cached config.
|
||||
*
|
||||
* @return bool True if the cached fraud services config is valid.
|
||||
*/
|
||||
public function is_valid_cached_fraud_services( $fraud_services_config ): bool {
|
||||
// null/false means no config has been cached.
|
||||
if ( null === $fraud_services_config || false === $fraud_services_config ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-array values are not expected.
|
||||
if ( ! is_array( $fraud_services_config ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and caches the public fraud services config.
|
||||
*
|
||||
* @return array|null Fraud services config or null if failed to retrieve fraud services config.
|
||||
*/
|
||||
private function get_cached_fraud_services(): ?array {
|
||||
return $this->database_cache->get_or_add(
|
||||
Database_Cache::FRAUD_SERVICES_KEY,
|
||||
function () {
|
||||
$fraud_services = $this->fetch_public_fraud_services_config();
|
||||
|
||||
if ( ! $this->is_valid_cached_fraud_services( $fraud_services ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sanitize the config just to be safe by applying a sweeping `sanitize_text_field` on all the data.
|
||||
// This is OK to do since we are not accepting data entries with HTML.
|
||||
return WC_Payments_Utils::array_map_recursive(
|
||||
$fraud_services,
|
||||
function ( $value ) {
|
||||
// Only apply `sanitize_text_field()` to string values since this function will cast to string.
|
||||
if ( is_string( $value ) ) {
|
||||
return sanitize_text_field( $value );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
},
|
||||
[ $this, 'is_valid_cached_fraud_services' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the fraud config for a service.
|
||||
*
|
||||
* @param array $config Existing config data for the given anti-fraud service.
|
||||
* @param string $service_id Identifier of the anti-fraud service provider.
|
||||
*
|
||||
* @return array|NULL Array with all the required data to initialize the anti-fraud script, or NULL if the service shouldn't be used.
|
||||
*/
|
||||
private function prepare_fraud_config( array $config, string $service_id ): ?array {
|
||||
switch ( $service_id ) {
|
||||
case 'sift':
|
||||
return $this->prepare_sift_config( $config );
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds site-specific config needed to initialize the SIFT anti-fraud JS.
|
||||
*
|
||||
* @param array $config Associative array with the SIFT-related configuration returned from the server.
|
||||
*
|
||||
* @return array|null Assoc array, ready for the client to consume, or NULL if the client shouldn't enqueue this script.
|
||||
*/
|
||||
private function prepare_sift_config( array $config ): ?array {
|
||||
$test_mode = false;
|
||||
try {
|
||||
$test_mode = WC_Payments::mode()->is_test();
|
||||
} catch ( Exception $e ) {
|
||||
Logger::log( sprintf( 'WooPayments JS settings: Could not determine if WCPay should be in test mode! Message: %s', $e->getMessage() ), 'warning' );
|
||||
}
|
||||
|
||||
// The server returns both production and sandbox beacon keys. Use the sandbox one if test mode is enabled.
|
||||
if ( $test_mode && isset( $config['sandbox_beacon_key'] ) ) {
|
||||
$config['beacon_key'] = $config['sandbox_beacon_key'];
|
||||
}
|
||||
unset( $config['sandbox_beacon_key'] );
|
||||
|
||||
$config['user_id'] = $this->get_sift_user_id();
|
||||
$config['session_id'] = $this->session_service->get_sift_session_id();
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Sift user ID to use for the current request.
|
||||
*
|
||||
* @return string The Sift user ID to use for the current request. Empty string if we couldn't determine one.
|
||||
*/
|
||||
private function get_sift_user_id(): string {
|
||||
$user_id = '';
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
if ( is_admin() ) {
|
||||
// In the WP admin we deal with merchant accounts, not customers.
|
||||
$user_id = $this->account->get_stripe_account_id() ?? '';
|
||||
} else {
|
||||
$customer_id = $this->customer_service->get_customer_id_by_user_id( get_current_user_id() );
|
||||
if ( isset( $customer_id ) ) {
|
||||
$user_id = $customer_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches public fraud services config from the server.
|
||||
*
|
||||
* @return array|null Fraud services config or null.
|
||||
*/
|
||||
private function fetch_public_fraud_services_config(): ?array {
|
||||
// Build the endpoint URL.
|
||||
$url = WC_Payments_API_Client::ENDPOINT_BASE . '/' . WC_Payments_API_Client::ENDPOINT_REST_BASE . '/' . WC_Payments_API_Client::FRAUD_SERVICES_API;
|
||||
|
||||
$response = wp_remote_get(
|
||||
$url,
|
||||
[
|
||||
'user-agent' => 'WCPay/' . WCPAY_VERSION_NUMBER . '; ' . get_bloginfo( 'url' ),
|
||||
]
|
||||
);
|
||||
|
||||
// Return early if there is an error.
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = null;
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
// Decode the results, falling back to an empty array.
|
||||
$config = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( empty( $config ) || ! is_array( $config ) ) {
|
||||
$config = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Incentives_Service
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WCPay\Database_Cache;
|
||||
|
||||
/**
|
||||
* Class handling onboarding related business logic.
|
||||
*/
|
||||
class WC_Payments_Incentives_Service {
|
||||
|
||||
/**
|
||||
* Cache util for managing onboarding data.
|
||||
*
|
||||
* @var Database_Cache
|
||||
*/
|
||||
private $database_cache;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param Database_Cache $database_cache Database cache util.
|
||||
*/
|
||||
public function __construct( Database_Cache $database_cache ) {
|
||||
$this->database_cache = $database_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise class hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'admin_menu', [ $this, 'add_payments_menu_badge' ] );
|
||||
add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] );
|
||||
add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] );
|
||||
add_filter( 'woocommerce_admin_woopayments_onboarding_task_additional_data', [ $this, 'onboarding_task_additional_data' ], 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add badge to payments menu if there is an eligible incentive.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_payments_menu_badge(): void {
|
||||
global $menu;
|
||||
|
||||
if ( ! $this->get_cached_connect_incentive() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge = WC_Payments_Admin::MENU_NOTIFICATION_BADGE;
|
||||
foreach ( $menu as $index => $menu_item ) {
|
||||
if ( false === strpos( $menu_item[0], $badge ) && ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) ) {
|
||||
$menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
|
||||
// One menu item with a badge is more than enough.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds allowed promo notes from eligible incentive.
|
||||
*
|
||||
* @param array $promo_notes Current allowed promo notes.
|
||||
* @return array Updated allowed promo notes.
|
||||
*/
|
||||
public function allowed_promo_notes( $promo_notes = [] ): array {
|
||||
$incentive = $this->get_cached_connect_incentive();
|
||||
// Return early if there is no eligible incentive.
|
||||
if ( empty( $incentive['id'] ) ) {
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress psalm error because we already check that `id` exists in `is_valid_cached_incentive`.
|
||||
*
|
||||
* @psalm-suppress PossiblyNullArrayAccess */
|
||||
$promo_notes[] = $incentive['id'];
|
||||
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the WooPayments incentive badge to the onboarding task.
|
||||
*
|
||||
* @param string $badge Current badge.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function onboarding_task_badge( string $badge ): string {
|
||||
$incentive = $this->get_cached_connect_incentive();
|
||||
// Return early if there is no eligible incentive.
|
||||
if ( empty( $incentive['id'] ) ) {
|
||||
return $badge;
|
||||
}
|
||||
|
||||
return $incentive['task_badge'] ?? $badge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the onboarding task additional data to add the WooPayments incentive data to it.
|
||||
*
|
||||
* @param ?array $additional_data The current task additional data.
|
||||
*
|
||||
* @return ?array The filtered task additional data.
|
||||
*/
|
||||
public function onboarding_task_additional_data( ?array $additional_data ): ?array {
|
||||
$incentive = $this->get_cached_connect_incentive();
|
||||
// Return early if there is no eligible incentive.
|
||||
if ( empty( $incentive['id'] ) ) {
|
||||
return $additional_data;
|
||||
}
|
||||
|
||||
if ( empty( $additional_data ) ) {
|
||||
$additional_data = [];
|
||||
}
|
||||
$additional_data['wooPaymentsIncentiveId'] = $incentive['id'];
|
||||
|
||||
return $additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and caches eligible connect incentive from the server.
|
||||
*
|
||||
* @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache.
|
||||
*
|
||||
* @return array|null List of incentives or null.
|
||||
*/
|
||||
public function get_cached_connect_incentive( bool $force_refresh = false ): ?array {
|
||||
// Return early if there is an account connected.
|
||||
if ( WC_Payments::get_account_service()->is_stripe_connected() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return early if the country is not supported.
|
||||
if ( ! array_key_exists( WC()->countries->get_base_country(), WC_Payments_Utils::supported_countries() ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fingerprint the store context through a hash of certain entries.
|
||||
$store_context_hash = $this->generate_context_hash( $this->get_store_context() );
|
||||
|
||||
// First, get the cache contents, if any.
|
||||
$incentive_data = $this->database_cache->get( Database_Cache::CONNECT_INCENTIVE_KEY, true );
|
||||
// Check if we need to force-refresh the cache contents.
|
||||
if ( empty( $incentive_data['context_hash'] ) || ! is_string( $incentive_data['context_hash'] )
|
||||
|| ! hash_equals( $store_context_hash, $incentive_data['context_hash'] ) ) {
|
||||
|
||||
// No cache, the hash is missing, or it doesn't match. Force refresh.
|
||||
$incentive_data = $this->database_cache->get_or_add(
|
||||
Database_Cache::CONNECT_INCENTIVE_KEY,
|
||||
[ $this, 'fetch_connect_incentive_details' ],
|
||||
'is_array',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $this->is_valid_cached_incentive( $incentive_data ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $incentive_data['incentive'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches eligible connect incentive details from the server.
|
||||
*
|
||||
* @return array|null Array of eligible incentive data or null.
|
||||
*/
|
||||
public function fetch_connect_incentive_details(): ?array {
|
||||
$store_context = $this->get_store_context();
|
||||
|
||||
// Request incentive from WCPAY API.
|
||||
$url = add_query_arg(
|
||||
$store_context,
|
||||
'https://public-api.wordpress.com/wpcom/v2/wcpay/incentives'
|
||||
);
|
||||
|
||||
$response = wp_remote_get(
|
||||
$url,
|
||||
[
|
||||
'user-agent' => 'WCPay/' . WCPAY_VERSION_NUMBER . '; ' . get_bloginfo( 'url' ),
|
||||
]
|
||||
);
|
||||
|
||||
// Return early if there is an error.
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$incentive = [];
|
||||
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
// Decode the results, falling back to an empty array.
|
||||
$results = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
// Find a `connect_page` incentive.
|
||||
if ( ! empty( $results ) ) {
|
||||
$incentive = array_filter(
|
||||
$results,
|
||||
function ( array $incentive ) {
|
||||
return isset( $incentive['type'] ) && 'connect_page' === $incentive['type'];
|
||||
}
|
||||
)[0] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// Read TTL form the `cache-for` header, or default to 1 day.
|
||||
$cache_for = wp_remote_retrieve_header( $response, 'cache-for' );
|
||||
|
||||
$incentive_data = [
|
||||
'incentive' => $incentive,
|
||||
];
|
||||
// Set the Time-To-Live for the incentive data.
|
||||
if ( '' !== $cache_for ) {
|
||||
$incentive_data['ttl'] = (int) $cache_for;
|
||||
} else {
|
||||
$incentive_data['ttl'] = DAY_IN_SECONDS;
|
||||
}
|
||||
|
||||
// Attach the context hash to the incentive data.
|
||||
$incentive_data['context_hash'] = $this->generate_context_hash( $store_context );
|
||||
|
||||
return $incentive_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the incentive data fetched from the cache are valid.
|
||||
* Expects an array with an `incentive` entry that is an array with at least `id`, `description`, and `tc_url` keys.
|
||||
*
|
||||
* @param mixed $incentive_data The incentive data returned from the cache.
|
||||
*
|
||||
* @return bool Whether the incentive data is valid.
|
||||
*/
|
||||
public function is_valid_cached_incentive( $incentive_data ): bool {
|
||||
if ( ! is_array( $incentive_data )
|
||||
|| empty( $incentive_data['incentive'] )
|
||||
|| ! is_array( $incentive_data['incentive'] )
|
||||
|| ! isset( $incentive_data['incentive']['id'] )
|
||||
|| ! isset( $incentive_data['incentive']['description'] )
|
||||
|| ! isset( $incentive_data['incentive']['tc_url'] ) ) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WooPayments payment gateway is active and set up,
|
||||
* or there are orders processed with it, at some moment.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_wcpay(): bool {
|
||||
// We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache.
|
||||
// This implies that WooPayments is or was active at some point and that it was connected.
|
||||
if ( $this->has_wcpay_account_data() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there is at least one order processed with WooPayments, we consider the store to have WooPayments.
|
||||
if ( ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'payment_method' => 'woocommerce_payments',
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
]
|
||||
)
|
||||
) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is meaningful data in the WooPayments account cache.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_wcpay_account_data(): bool {
|
||||
$account_data = $this->database_cache->get( Database_Cache::ACCOUNT_KEY );
|
||||
if ( ! empty( $account_data['account_id'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store context to be used in determining eligibility.
|
||||
*
|
||||
* @return array The store context.
|
||||
*/
|
||||
private function get_store_context(): array {
|
||||
return [
|
||||
// Store ISO-2 country code, e.g. `US`.
|
||||
'country' => WC()->countries->get_base_country(),
|
||||
// Store locale, e.g. `en_US`.
|
||||
'locale' => get_locale(),
|
||||
// WooCommerce active for duration in seconds.
|
||||
'active_for' => time() - get_option( 'woocommerce_admin_install_timestamp', time() ),
|
||||
// Whether the store has paid orders in the last 90 days.
|
||||
'has_orders' => ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'status' => [ 'wc-completed', 'wc-processing' ],
|
||||
'date_created' => '>=' . strtotime( '-90 days' ),
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
]
|
||||
)
|
||||
),
|
||||
// Whether the store has at least one payment gateway enabled.
|
||||
'has_payments' => ! empty( WC()->payment_gateways()->get_available_payment_gateways() ),
|
||||
'has_wcpay' => $this->has_wcpay(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash from the store context data.
|
||||
*
|
||||
* @param array $context The store context data.
|
||||
*
|
||||
* @return string The context hash.
|
||||
*/
|
||||
private function generate_context_hash( array $context ): string {
|
||||
// Include only certain entries in the context hash.
|
||||
// We need only discrete, user-interaction dependent data.
|
||||
// Entries like `active_for` have no place in the hash generation since they change automatically.
|
||||
return md5(
|
||||
wp_json_encode(
|
||||
[
|
||||
'country' => $context['country'] ?? '',
|
||||
'locale' => $context['locale'] ?? '',
|
||||
'has_orders' => $context['has_orders'] ?? false,
|
||||
'has_payments' => $context['has_payments'] ?? false,
|
||||
'has_wcpay' => $context['has_wcpay'] ?? false,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments WC_Payments_Localization_Service Class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC_Payments_Localization_Service.
|
||||
*/
|
||||
class WC_Payments_Localization_Service implements MultiCurrencyLocalizationInterface {
|
||||
const WCPAY_CURRENCY_FORMAT_TRANSIENT = 'wcpay_currency_format';
|
||||
const WCPAY_LOCALE_INFO_TRANSIENT = 'wcpay_locale_info';
|
||||
|
||||
/**
|
||||
* Currency formatting map.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $currency_format = [];
|
||||
|
||||
/**
|
||||
* Currency locale info.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $locale_info = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->load_locale_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's format from mapped data.
|
||||
*
|
||||
* @param string $currency_code The currency code.
|
||||
*
|
||||
* @return array The currency's format.
|
||||
*/
|
||||
public function get_currency_format( $currency_code ): array {
|
||||
// Default to USD settings if mapping not found.
|
||||
$currency_format = [
|
||||
'currency_pos' => 'left',
|
||||
'thousand_sep' => ',',
|
||||
'decimal_sep' => '.',
|
||||
'num_decimals' => 2,
|
||||
];
|
||||
|
||||
$locale = $this->get_user_locale();
|
||||
|
||||
$currency_options = $this->currency_format[ $currency_code ] ?? null;
|
||||
if ( $currency_options ) {
|
||||
// If there's no locale-specific formatting, default to the 'default' entry in the array.
|
||||
$currency_format = $currency_options[ $locale ] ?? $currency_options['default'] ?? $currency_format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to edit formatting for a specific currency (wcpay_{currency_code}_format).
|
||||
*
|
||||
* This filter can be used to override the currency format for a specific currency.
|
||||
* The currency code in the filter name should be used in lowercase.
|
||||
*
|
||||
* @since 2.8.0
|
||||
*
|
||||
* @param array $currency_format The currency format settings.
|
||||
* @param string $locale The user's locale.
|
||||
*/
|
||||
return apply_filters( 'wcpay_' . strtolower( $currency_code ) . '_format', $currency_format, $locale );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user locale.
|
||||
*
|
||||
* @return string The locale.
|
||||
*/
|
||||
public function get_user_locale(): string {
|
||||
return get_user_locale();
|
||||
}
|
||||
|
||||
// TODO: Add tests.
|
||||
/**
|
||||
* Returns the locale data for a country.
|
||||
*
|
||||
* @param string $country Country code.
|
||||
*
|
||||
* @return array Array with the country's locale data. Empty array if country not found.
|
||||
*/
|
||||
public function get_country_locale_data( $country ): array {
|
||||
return $this->locale_info[ $country ] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads locale data from WooCommerce core (/i18n/locale-info.php) and maps it
|
||||
* to be used by currency.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function load_locale_data() {
|
||||
$transient_currency_format_data = get_transient( self::WCPAY_CURRENCY_FORMAT_TRANSIENT );
|
||||
$transient_locale_info_data = get_transient( self::WCPAY_LOCALE_INFO_TRANSIENT );
|
||||
|
||||
if ( $transient_currency_format_data && $transient_locale_info_data ) {
|
||||
$this->currency_format = $transient_currency_format_data;
|
||||
$this->locale_info = $transient_locale_info_data;
|
||||
return;
|
||||
}
|
||||
|
||||
$locale_info_path = WC()->plugin_path() . '/i18n/locale-info.php';
|
||||
|
||||
// The full locale data was introduced in the currency-info.php file.
|
||||
// If it doesn't exist we have to use the fallback.
|
||||
if ( ! file_exists( WC()->plugin_path() . '/i18n/currency-info.php' ) ) {
|
||||
$locale_info_path = WCPAY_ABSPATH . 'i18n/locale-info.php';
|
||||
}
|
||||
|
||||
$this->locale_info = include $locale_info_path;
|
||||
|
||||
if ( is_array( $this->locale_info ) && 0 < count( $this->locale_info ) ) {
|
||||
// Extract the currency formatting options from the locale info.
|
||||
foreach ( $this->locale_info as $country_data ) {
|
||||
$currency_code = $country_data['currency_code'];
|
||||
|
||||
foreach ( $country_data['locales'] as $locale => $locale_data ) {
|
||||
if ( empty( $locale_data ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->currency_format[ $currency_code ][ $locale ] = [
|
||||
'currency_pos' => $locale_data['currency_pos'],
|
||||
'thousand_sep' => $locale_data['thousand_sep'],
|
||||
'decimal_sep' => $locale_data['decimal_sep'],
|
||||
'num_decimals' => $country_data['num_decimals'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
set_transient( self::WCPAY_CURRENCY_FORMAT_TRANSIENT, $this->currency_format, DAY_IN_SECONDS );
|
||||
set_transient( self::WCPAY_LOCALE_INFO_TRANSIENT, $this->locale_info, DAY_IN_SECONDS );
|
||||
}
|
||||
}
|
||||
}
|
||||
+1121
File diff suppressed because it is too large
Load Diff
+2130
File diff suppressed because it is too large
Load Diff
+303
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Order_Success_Page
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Constants\Payment_Method;
|
||||
use WCPay\Duplicate_Payment_Prevention_Service;
|
||||
|
||||
/**
|
||||
* Class handling order success page.
|
||||
*/
|
||||
class WC_Payments_Order_Success_Page {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_order_received_verify_known_shoppers', [ $this, 'determine_woopay_order_received_verify_known_shoppers' ], 11 );
|
||||
add_action( 'woocommerce_before_thankyou', [ $this, 'register_payment_method_override' ] );
|
||||
add_action( 'woocommerce_order_details_before_order_table', [ $this, 'unregister_payment_method_override' ] );
|
||||
add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_paid_order' ], 11 );
|
||||
add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_successful_intent' ], 11 );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the hook to override the payment method name on the order received page.
|
||||
*/
|
||||
public function register_payment_method_override() {
|
||||
// Override the payment method title on the order received page.
|
||||
add_filter( 'woocommerce_order_get_payment_method_title', [ $this, 'show_woocommerce_payments_payment_method_name' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the hook to override the payment method name on the order received page before the order summary.
|
||||
*/
|
||||
public function unregister_payment_method_override() {
|
||||
remove_filter( 'woocommerce_order_get_payment_method_title', [ $this, 'show_woocommerce_payments_payment_method_name' ], 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked into `woocommerce_order_get_payment_method_title` to change the payment method title on the
|
||||
* order received page for WooPay and BNPL orders.
|
||||
*
|
||||
* @param string $payment_method_title Original payment method title.
|
||||
* @param WC_Abstract_Order $abstract_order Successful received order being shown.
|
||||
* @return string
|
||||
*/
|
||||
public function show_woocommerce_payments_payment_method_name( $payment_method_title, $abstract_order ) {
|
||||
// Only change the payment method title on the order received page.
|
||||
if ( ! is_order_received_page() ) {
|
||||
return $payment_method_title;
|
||||
}
|
||||
|
||||
$order_id = $abstract_order->get_id();
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return $payment_method_title;
|
||||
}
|
||||
|
||||
$payment_method_id = $order->get_payment_method();
|
||||
|
||||
if ( stripos( $payment_method_id, 'woocommerce_payments' ) !== 0 ) {
|
||||
return $payment_method_title;
|
||||
}
|
||||
|
||||
// If this is a WooPay order, return the html for the WooPay payment method name.
|
||||
if ( $order->get_meta( 'is_woopay' ) ) {
|
||||
return $this->show_woopay_payment_method_name( $order );
|
||||
}
|
||||
|
||||
$gateway = WC()->payment_gateways()->payment_gateways()[ $payment_method_id ];
|
||||
|
||||
if ( ! is_object( $gateway ) || ! method_exists( $gateway, 'get_payment_method' ) ) {
|
||||
return $payment_method_title;
|
||||
}
|
||||
|
||||
$payment_method = $gateway->get_payment_method( $order );
|
||||
// GooglePay/ApplePay/Link/Card to be supported later.
|
||||
if ( $payment_method->get_id() === Payment_Method::CARD ) {
|
||||
return $this->show_card_payment_method_name( $order, $payment_method );
|
||||
}
|
||||
|
||||
// If this is an LPM (BNPL or local payment method) order, return the html for the payment method name.
|
||||
$name_output = $this->show_lpm_payment_method_name( $gateway, $payment_method );
|
||||
|
||||
if ( false !== $name_output ) {
|
||||
return $name_output;
|
||||
}
|
||||
|
||||
return $payment_method_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML to add the card brand logo and the last 4 digits of the card used to the
|
||||
* payment method name on the order received page.
|
||||
*
|
||||
* @param WC_Order $order the order being shown.
|
||||
* @param WCPay\Payment_Methods\UPE_Payment_Method $payment_method the payment method being shown.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function show_card_payment_method_name( $order, $payment_method ) {
|
||||
$card_brand = $order->get_meta( '_card_brand' );
|
||||
|
||||
if ( ! $card_brand ) {
|
||||
return $payment_method->get_title();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wc-payment-gateway-method-logo-wrapper wc-payment-card-logo">
|
||||
<img alt="<?php echo esc_attr( $payment_method->get_title() ); ?>" src="<?php echo esc_url_raw( plugins_url( "assets/images/cards/{$card_brand}.svg", WCPAY_PLUGIN_FILE ) ); ?>">
|
||||
<?php
|
||||
if ( $order->get_meta( 'last4' ) ) {
|
||||
echo esc_html_e( '•••', 'woocommerce-payments' ) . ' ';
|
||||
echo esc_html( $order->get_meta( 'last4' ) );
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML to add the WooPay logo and the last 4 digits of the card used to the
|
||||
* payment method name on the order received page.
|
||||
*
|
||||
* @param WC_Order $order the order being shown.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function show_woopay_payment_method_name( $order ) {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wc-payment-gateway-method-logo-wrapper woopay">
|
||||
<img alt="WooPay" src="<?php echo esc_url_raw( plugins_url( 'assets/images/woopay.svg', WCPAY_PLUGIN_FILE ) ); ?>">
|
||||
<?php
|
||||
if ( $order->get_meta( 'last4' ) ) {
|
||||
echo esc_html_e( 'Card ending in', 'woocommerce-payments' ) . ' ';
|
||||
echo esc_html( $order->get_meta( 'last4' ) );
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the LPM logo to the payment method name on the order received page.
|
||||
*
|
||||
* @param WC_Payment_Gateway_WCPay $gateway the gateway being shown.
|
||||
* @param WCPay\Payment_Methods\UPE_Payment_Method $payment_method the payment method being shown.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function show_lpm_payment_method_name( $gateway, $payment_method ) {
|
||||
$method_logo_url = apply_filters_deprecated(
|
||||
'wc_payments_thank_you_page_bnpl_payment_method_logo_url',
|
||||
[
|
||||
$payment_method->get_payment_method_icon_for_location( 'checkout', false, $gateway->get_account_country() ),
|
||||
$payment_method->get_id(),
|
||||
],
|
||||
'8.5.0',
|
||||
'wc_payments_thank_you_page_lpm_payment_method_logo_url'
|
||||
);
|
||||
$method_logo_url = apply_filters(
|
||||
'wc_payments_thank_you_page_lpm_payment_method_logo_url',
|
||||
$method_logo_url,
|
||||
$payment_method->get_id()
|
||||
);
|
||||
|
||||
// If we don't have a logo URL here for some reason, bail.
|
||||
if ( ! $method_logo_url ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wc-payment-gateway-method-logo-wrapper wc-payment-lpm-logo wc-payment-lpm-logo--<?php echo esc_attr( $payment_method->get_id() ); ?>">
|
||||
<img alt="<?php echo esc_attr( $payment_method->get_title() ); ?>" src="<?php echo esc_url_raw( $method_logo_url ); ?>">
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the notice to the thank you page in case a recent order with the same content has already paid.
|
||||
*
|
||||
* @param string $text the default thank you text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function add_notice_previous_paid_order( $text ) {
|
||||
if ( isset( $_GET[ Duplicate_Payment_Prevention_Service::FLAG_PREVIOUS_ORDER_PAID ] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
$text .= $this->format_addtional_thankyou_order_received_text(
|
||||
__( 'We detected and prevented an attempt to pay for a duplicate order. If this was a mistake and you wish to try again, please create a new order.', 'woocommerce-payments' )
|
||||
);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the notice to the thank you page in case an existing intention was successful for the order.
|
||||
*
|
||||
* @param string $text the default thank you text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function add_notice_previous_successful_intent( $text ) {
|
||||
if ( isset( $_GET[ Duplicate_Payment_Prevention_Service::FLAG_PREVIOUS_SUCCESSFUL_INTENT ] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
$text .= $this->format_addtional_thankyou_order_received_text(
|
||||
__( 'We prevented multiple payments for the same order. If this was a mistake and you wish to try again, please create a new order.', 'woocommerce-payments' )
|
||||
);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats the additional text to be displayed on the thank you page, with the side effect
|
||||
* as a workaround for an issue in Woo core 8.1.x and 8.2.x.
|
||||
*
|
||||
* @param string $additional_text The additional text to be displayed.
|
||||
*
|
||||
* @return string Formatted text.
|
||||
*/
|
||||
private function format_addtional_thankyou_order_received_text( string $additional_text ): string {
|
||||
/**
|
||||
* This condition is a workaround for Woo core 8.1.x and 8.2.x as it formatted the filtered text,
|
||||
* while it should format the original text only.
|
||||
*
|
||||
* It's safe to remove this conditional when WooPayments requires Woo core 8.3.x or higher.
|
||||
*
|
||||
* @see https://github.com/woocommerce/woocommerce/pull/39758 Introduce the issue since 8.1.0.
|
||||
* @see https://github.com/woocommerce/woocommerce/pull/40353 Fix the issue since 8.3.0.
|
||||
*/
|
||||
if ( version_compare( WC_VERSION, '8.0', '>' )
|
||||
&& version_compare( WC_VERSION, '8.3', '<' )
|
||||
) {
|
||||
echo "
|
||||
<script type='text/javascript'>
|
||||
document.querySelector('.woocommerce-thankyou-order-received')?.classList?.add('woocommerce-info');
|
||||
</script>
|
||||
";
|
||||
|
||||
return ' ' . $additional_text;
|
||||
}
|
||||
|
||||
return sprintf( '<div class="woocommerce-info">%s</div>', $additional_text );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue style to the order success page
|
||||
*/
|
||||
public function enqueue_scripts() {
|
||||
if ( ! is_order_received_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC_Payments_Utils::enqueue_style(
|
||||
'wcpay-success-css',
|
||||
plugins_url( 'assets/css/success.css', WCPAY_PLUGIN_FILE ),
|
||||
[],
|
||||
WC_Payments::get_file_version( 'assets/css/success.css' ),
|
||||
'all',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure we show the TYP page for orders paid with WooPay
|
||||
* that create new user accounts, code mainly copied from
|
||||
* WooCommerce WC_Shortcode_Checkout::order_received and
|
||||
* WC_Shortcode_Checkout::guest_should_verify_email.
|
||||
*
|
||||
* @param bool $value The current value for this filter.
|
||||
*/
|
||||
public function determine_woopay_order_received_verify_known_shoppers( $value ) {
|
||||
global $wp;
|
||||
|
||||
$order_id = $wp->query_vars['order-received'];
|
||||
$order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) );
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ( ! $order instanceof WC_Order ) || ! $order->get_meta( 'is_woopay' ) || ! hash_equals( $order->get_order_key(), $order_key ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order );
|
||||
$date_created = $order->get_date_created();
|
||||
|
||||
// We do not need to verify the email address if we are within the grace period immediately following order creation.
|
||||
$is_within_grace_period = is_a( $date_created, \WC_DateTime::class, true )
|
||||
&& time() - $date_created->getTimestamp() <= $verification_grace_period;
|
||||
|
||||
return ! $is_within_grace_period;
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Payment_Method_Messaging_Element
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Constants\Payment_Method;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* WC_Payments_Payment_Method_Messaging_Element class.
|
||||
*/
|
||||
class WC_Payments_Payment_Method_Messaging_Element {
|
||||
/**
|
||||
* WC_Payments_Account instance to get information about the account.
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
/**
|
||||
* WC_Payments_Gateway instance to get information about the enabled payment methods.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* WC_Payments_Payment_Method_Messaging_Element constructor
|
||||
*
|
||||
* @param WC_Payments_Account $account Account instance.
|
||||
* @param WC_Payment_Gateway_WCPay $gateway Gateway instance.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway ) {
|
||||
$this->account = $account;
|
||||
$this->gateway = $gateway;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the payment method messaging element.
|
||||
*
|
||||
* @return string|void The HTML markup for the payment method message container.
|
||||
*/
|
||||
public function init() {
|
||||
|
||||
$is_cart_block = WC_Payments_Utils::is_cart_block();
|
||||
|
||||
if ( ! is_product() && ! is_cart() && ! $is_cart_block ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $product;
|
||||
$currency_code = get_woocommerce_currency();
|
||||
$store_country = WC()->countries->get_base_country();
|
||||
$billing_country = WC()->customer->get_billing_country();
|
||||
$cart_total = WC()->cart->total;
|
||||
$product_variations = [];
|
||||
|
||||
if ( $product ) {
|
||||
$get_price_fn = function ( $product ) {
|
||||
return $product->get_price();
|
||||
};
|
||||
if ( wc_tax_enabled() && $product->is_taxable() ) {
|
||||
if (
|
||||
wc_prices_include_tax() &&
|
||||
(
|
||||
get_option( 'woocommerce_tax_display_shop' ) !== 'incl' ||
|
||||
WC()->customer->get_is_vat_exempt()
|
||||
)
|
||||
) {
|
||||
$get_price_fn = function ( $product ) {
|
||||
return wc_get_price_excluding_tax( $product );
|
||||
};
|
||||
} elseif (
|
||||
get_option( 'woocommerce_tax_display_shop' ) === 'incl'
|
||||
&& ! WC()->customer->get_is_vat_exempt()
|
||||
) {
|
||||
$get_price_fn = function ( $product ) {
|
||||
return wc_get_price_including_tax( $product );
|
||||
};
|
||||
}
|
||||
}
|
||||
$price = $get_price_fn( $product );
|
||||
$product_variations = [
|
||||
'base_product' => [
|
||||
'amount' => WC_Payments_Utils::prepare_amount( $price, $currency_code ),
|
||||
'currency' => $currency_code,
|
||||
],
|
||||
];
|
||||
|
||||
$product_price = $product_variations['base_product']['amount'];
|
||||
|
||||
foreach ( $product->get_children() as $variation_id ) {
|
||||
$variation = wc_get_product( $variation_id );
|
||||
if ( $variation ) {
|
||||
$price = $get_price_fn( $variation );
|
||||
$product_variations[ $variation_id ] = [
|
||||
'amount' => WC_Payments_Utils::prepare_amount( $price, $currency_code ),
|
||||
'currency' => $currency_code,
|
||||
];
|
||||
|
||||
$product_price = $product_variations['base_product']['amount'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$enabled_upe_payment_methods = $this->gateway->get_upe_enabled_payment_method_ids();
|
||||
// Filter non BNPL out of the list of payment methods.
|
||||
$bnpl_payment_methods = array_intersect( $enabled_upe_payment_methods, Payment_Method::BNPL_PAYMENT_METHODS );
|
||||
|
||||
// register the script.
|
||||
WC_Payments::register_script_with_dependencies( 'WCPAY_PRODUCT_DETAILS', 'dist/product-details', [ 'stripe' ] );
|
||||
wp_enqueue_script( 'WCPAY_PRODUCT_DETAILS' );
|
||||
|
||||
// Enqueue the styles.
|
||||
wp_enqueue_style(
|
||||
'wcpay-product-details',
|
||||
plugins_url( 'dist/product-details.css', WCPAY_PLUGIN_FILE ),
|
||||
[],
|
||||
WC_Payments::get_file_version( 'dist/product-details.css' ),
|
||||
);
|
||||
|
||||
$country = empty( $billing_country ) ? $store_country : $billing_country;
|
||||
|
||||
$script_data = [
|
||||
'productId' => 'base_product',
|
||||
'productVariations' => $product_variations,
|
||||
'country' => $country,
|
||||
'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ),
|
||||
'accountId' => $this->account->get_stripe_account_id(),
|
||||
'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ),
|
||||
'paymentMethods' => array_values( $bnpl_payment_methods ),
|
||||
'currencyCode' => $currency_code,
|
||||
'isCart' => is_cart(),
|
||||
'isCartBlock' => $is_cart_block,
|
||||
'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ),
|
||||
'nonce' => [
|
||||
'get_cart_total' => wp_create_nonce( 'wcpay-get-cart-total' ),
|
||||
'is_bnpl_available' => wp_create_nonce( 'wcpay-is-bnpl-available' ),
|
||||
],
|
||||
'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
|
||||
];
|
||||
|
||||
if ( $product ) {
|
||||
$script_data['isBnplAvailable'] = WC_Payments_Utils::is_any_bnpl_method_available( array_values( $bnpl_payment_methods ), $country, $currency_code, $product_price );
|
||||
}
|
||||
|
||||
// Create script tag with config.
|
||||
wp_localize_script(
|
||||
'WCPAY_PRODUCT_DETAILS',
|
||||
'wcpayStripeSiteMessaging',
|
||||
$script_data
|
||||
);
|
||||
|
||||
// Ensure wcpayConfig is available in the page.
|
||||
$wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) );
|
||||
wp_add_inline_script(
|
||||
'WCPAY_PRODUCT_DETAILS',
|
||||
"
|
||||
var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) );
|
||||
",
|
||||
'before'
|
||||
);
|
||||
|
||||
if ( ! $is_cart_block ) {
|
||||
return '<div id="payment-method-message"></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Payment_Request_Session_Handler
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
|
||||
|
||||
/**
|
||||
* WC_Payments_Payment_Request_Session_Handler class
|
||||
*/
|
||||
final class WC_Payments_Payment_Request_Session_Handler extends WC_Session_Handler {
|
||||
/**
|
||||
* Token from HTTP headers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/**
|
||||
* The session id to reference in the sessions table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $session_id;
|
||||
|
||||
/**
|
||||
* Expiration timestamp.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $session_expiration;
|
||||
|
||||
/**
|
||||
* Constructor for the session class.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->token = wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] ?? '' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Init hooks and session data.
|
||||
*
|
||||
* @throws Exception On possible token mismatch.
|
||||
*/
|
||||
public function init() {
|
||||
$this->init_session_cookie();
|
||||
|
||||
if ( $this->_customer_id !== $this->_data['token_customer_id'] ) {
|
||||
throw new Exception( __( 'Invalid token: cookie and session customer mismatch', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
add_action( 'shutdown', [ $this, 'save_data' ], 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup cookie and customer ID.
|
||||
* We need to ensure that we _also_ call `init_session_from_token` when `init_session_cookie` is called.
|
||||
* Otherwise, this clears everything: https://github.com/woocommerce/woocommerce/blob/de4a8ffdd474ca1879d4aa16487d6c52472a861b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php#L556-L558
|
||||
*/
|
||||
public function init_session_cookie() {
|
||||
// If an account has been created after the session has been initialized, update the session.
|
||||
// This method is called directly by WC blocks when an account is created right before placing an order.
|
||||
$previous_session_data = null;
|
||||
parent::init_session_cookie();
|
||||
|
||||
if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
|
||||
$previous_session_data = $this->_data;
|
||||
/**
|
||||
* This is borrowed from WooCommerce core, which also converts the user ID to a string.
|
||||
* https://github.com/woocommerce/woocommerce/blob/f01e9452045e2d483649670adc2f042391774e38/plugins/woocommerce/includes/class-wc-session-handler.php#L107
|
||||
*
|
||||
* @psalm-suppress InvalidPropertyAssignmentValue
|
||||
*/
|
||||
$this->_customer_id = strval( get_current_user_id() );
|
||||
}
|
||||
|
||||
$this->init_session_from_token();
|
||||
if ( ! empty( $previous_session_data ) ) {
|
||||
$this->_data = $previous_session_data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the token header to load the correct session.
|
||||
*/
|
||||
protected function init_session_from_token() {
|
||||
$default_value = [
|
||||
'token_customer_id' => $this->_customer_id,
|
||||
];
|
||||
|
||||
if ( empty( $this->token ) ) {
|
||||
$this->session_id = $this->generate_customer_id();
|
||||
$this->_data = $default_value;
|
||||
// session_expiration can remain the same.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = JsonWebToken::get_parts( $this->token )->payload;
|
||||
|
||||
$this->session_id = $payload->session_id;
|
||||
$this->session_expiration = $payload->exp;
|
||||
$this->_data = (array) $this->get_session( $this->session_id, $default_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the session from the cache and database.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
*/
|
||||
public function delete_session( $customer_id ) {
|
||||
parent::delete_session( $this->session_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the session expiry timestamp.
|
||||
*
|
||||
* @param string $customer_id Customer ID.
|
||||
* @param int $timestamp Timestamp to expire the cookie.
|
||||
*/
|
||||
public function update_session_timestamp( $customer_id, $timestamp ) {
|
||||
parent::update_session_timestamp( $this->session_id, $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget all session data without destroying it.
|
||||
*/
|
||||
public function forget_session() {
|
||||
parent::forget_session();
|
||||
|
||||
$this->session_id = $this->generate_customer_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique customer ID for both logged-in and guest customers.
|
||||
*
|
||||
* Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate_customer_id() {
|
||||
require_once ABSPATH . 'wp-includes/class-phpass.php';
|
||||
$hasher = new PasswordHash( 8, false );
|
||||
|
||||
return 't_' . substr( md5( $hasher->get_random_bytes( 32 ) ), 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data - copy of parent method with a few modifications.
|
||||
*
|
||||
* @param int $old_session_key session ID before user logs in.
|
||||
*/
|
||||
public function save_data( $old_session_key = 0 ) {
|
||||
// Dirty if something changed - prevents saving nothing new.
|
||||
if ( $this->_dirty ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"INSERT INTO $this->_table (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d)
|
||||
ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)",
|
||||
$this->session_id,
|
||||
maybe_serialize( $this->_data ),
|
||||
$this->_session_expiration
|
||||
)
|
||||
);
|
||||
|
||||
wp_cache_set( $this->get_cache_prefix() . $this->session_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
|
||||
$this->_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_cache_prefix() {
|
||||
return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session variable.
|
||||
* Overridden default method, so that the `cart` session data always returns a value, in order to prevent the "saved cart after login" feature to get wrong cart data.
|
||||
* See "WC_Cart_Session::get_cart_from_session".
|
||||
*
|
||||
* @param string $key Key to get.
|
||||
* @param mixed $default used if the session variable isn't set.
|
||||
* @return array|string value of session variable
|
||||
*/
|
||||
public function get( $key, $default = null ) {
|
||||
if ( 'cart' === $key && ! isset( $this->_data['cart'] ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parent::get( $key, $default );
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Payment_Request_Session
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WC_Payments_Payment_Request_Session class.
|
||||
*/
|
||||
class WC_Payments_Payment_Request_Session {
|
||||
/**
|
||||
* Name of the parameter added to the "order received" page for orders placed with the custom session handler on product pages.
|
||||
*
|
||||
* @var string Name of the parameter.
|
||||
*/
|
||||
private static $prevent_empty_cart_parameter = 'woopayments-custom-session';
|
||||
|
||||
/**
|
||||
* Used as a temporary reference to cart data, so it can be restored later.
|
||||
*
|
||||
* @var null|WC_Cart Temporary reference to cart data.
|
||||
*/
|
||||
private $cart_clone = null;
|
||||
|
||||
/**
|
||||
* Init the hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// adding this filter with a higher priority than the session handler of the Store API.
|
||||
add_filter( 'woocommerce_session_handler', [ $this, 'add_payment_request_store_api_session_handler' ], 20 );
|
||||
add_filter( 'rest_post_dispatch', [ $this, 'store_api_headers' ], 10, 3 );
|
||||
|
||||
// checking to ensure we're not erasing the cart on the "order received" page.
|
||||
if ( $this->is_custom_session_order_received_page() ) {
|
||||
add_filter( 'woocommerce_persistent_cart_enabled', '__return_false' );
|
||||
add_filter( 'woocommerce_cart_session_initialize', '__return_false' );
|
||||
add_action(
|
||||
'woocommerce_before_cart_emptied',
|
||||
[ $this, 'save_old_cart_data_for_restore' ]
|
||||
);
|
||||
add_action(
|
||||
'woocommerce_cart_emptied',
|
||||
[ $this, 'restore_old_cart_data' ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an instance of the current cart, so it can be restored later.
|
||||
* Used on the "order received" page for orders placed with the PRB. The "order received" page empties the cart, otherwise.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save_old_cart_data_for_restore() {
|
||||
$this->cart_clone = clone WC()->cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the cart saved previously.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restore_old_cart_data() {
|
||||
if ( ! $this->cart_clone ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC()->cart->cart_contents = $this->cart_clone->cart_contents;
|
||||
WC()->cart->removed_cart_contents = $this->cart_clone->removed_cart_contents;
|
||||
WC()->cart->applied_coupons = $this->cart_clone->applied_coupons;
|
||||
|
||||
$this->cart_clone = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensuring that the return URL for the "order received" page contains a query string parameter
|
||||
* that can later be identified to ensure we don't clear the cart.
|
||||
* This function is only executed when we're using the custom session handler on Store API requests.
|
||||
*
|
||||
* @param string $return_url The URL for the "Order received" page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function store_api_order_received_return_url( $return_url ) {
|
||||
return add_query_arg( self::$prevent_empty_cart_parameter, '1', $return_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the $_SERVER global has order received URL slug in its 'REQUEST_URI' value - just like `wcs_is_order_received_page`.
|
||||
*
|
||||
* Similar to WooCommerce's is_custom_session_order_received_page(), but can be used before the $wp's query vars are setup, which is essential
|
||||
* when preventing the cart from being emptied on the "order received" page, if the order has been placed with WooPayments GooglePay/ApplePay on the product page.
|
||||
*
|
||||
* @return bool
|
||||
**/
|
||||
private function is_custom_session_order_received_page() {
|
||||
// ignoring because we're not storing the value anywhere, just checking its existence.
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
return ( false !== strpos( $_SERVER['REQUEST_URI'], 'order-received' ) ) && ( false !== strpos( $_SERVER['REQUEST_URI'], self::$prevent_empty_cart_parameter ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a session token for the response headers.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_session_token() {
|
||||
return JsonWebToken::create(
|
||||
[
|
||||
'session_id' => WC()->session->session_id,
|
||||
'exp' => time() + intval( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ),
|
||||
'iss' => 'woopayments/product-page',
|
||||
],
|
||||
'@' . wp_salt()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding the session key to the Store API response, to ensure the session can be retrieved later.
|
||||
*
|
||||
* @param mixed $response Response to replace the requested version with.
|
||||
* @param \WP_REST_Server $server Server instance.
|
||||
* @param \WP_REST_Request $request Request used to generate the response.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function store_api_headers( $response, $server, $request ) {
|
||||
if ( ! \WC_Payments_Utils::is_store_api_request() ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$nonce = $request->get_header( 'X-WooPayments-Tokenized-Cart-Session-Nonce' );
|
||||
if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_session_nonce' ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response->header( 'X-WooPayments-Tokenized-Cart-Session', $this->get_session_token() );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This filter is used to add a custom session handler before processing Store API request callbacks.
|
||||
* This is only necessary because the Store API SessionHandler currently doesn't provide an `init_session_cookie` method.
|
||||
*
|
||||
* @param string $default_session_handler The default session handler class name.
|
||||
*
|
||||
* @return string The session handler class name.
|
||||
*/
|
||||
public function add_payment_request_store_api_session_handler( $default_session_handler ) {
|
||||
$nonce = wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION_NONCE'] ?? null ) );
|
||||
if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_session_nonce' ) ) {
|
||||
return $default_session_handler;
|
||||
}
|
||||
|
||||
if ( ! \WC_Payments_Utils::is_store_api_request() ) {
|
||||
return $default_session_handler;
|
||||
}
|
||||
|
||||
if ( ! class_exists( JsonWebToken::class ) ) {
|
||||
return $default_session_handler;
|
||||
}
|
||||
|
||||
// checking if the token is valid, if it's provided.
|
||||
// there can also be a case where the token is not provided, but we should still use the custom session handler.
|
||||
$cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] ?? null ) );
|
||||
if (
|
||||
$cart_token && ! JsonWebToken::validate( $cart_token, '@' . wp_salt() )
|
||||
) {
|
||||
return $default_session_handler;
|
||||
}
|
||||
|
||||
// ensures cart contents aren't merged across different sessions for the same customer.
|
||||
add_filter( 'woocommerce_persistent_cart_enabled', '__return_false' );
|
||||
// when an order is placed via the Store API on product pages, we need to slightly modify the "order received" URL.
|
||||
add_filter( 'woocommerce_get_return_url', [ $this, 'store_api_order_received_return_url' ] );
|
||||
|
||||
require_once WCPAY_ABSPATH . '/includes/class-wc-payments-payment-request-session-handler.php';
|
||||
|
||||
return WC_Payments_Payment_Request_Session_Handler::class;
|
||||
}
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Redirect_Service
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Core\Server\Request\Get_Account_Capital_Link;
|
||||
use WCPay\Core\Server\Request\Get_Account_Login_Data;
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
use WCPay\Tracker;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class handling redirects business logic.
|
||||
*/
|
||||
class WC_Payments_Redirect_Service {
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* Constructor for WC_Payments_Session_Service.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $payments_api_client
|
||||
) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls wp_safe_redirect and exit.
|
||||
*
|
||||
* This method will end the execution immediately after the redirection.
|
||||
*
|
||||
* @param string $location The URL to redirect to.
|
||||
*/
|
||||
public function redirect_to( string $location ): void {
|
||||
wp_safe_redirect( $location );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to a wcpay-connect URL which then handles the next step for the onboarding flow.
|
||||
*
|
||||
* This is a sure way to ensure that the user is redirected to the correct URL to continue their onboarding.
|
||||
*
|
||||
* @param string $from Source of the redirect.
|
||||
* @param array $additional_params Optional. Additional URL params to add to the redirect URL.
|
||||
*/
|
||||
public function redirect_to_wcpay_connect( string $from = '', array $additional_params = [] ): void {
|
||||
// Take the user to the 'wcpay-connect' URL.
|
||||
// We handle creating and redirecting to the account link there.
|
||||
$params = [
|
||||
'wcpay-connect' => '1',
|
||||
'_wpnonce' => wp_create_nonce( 'wcpay-connect' ),
|
||||
];
|
||||
|
||||
$params = array_merge( $params, $additional_params );
|
||||
|
||||
if ( '' !== $from ) {
|
||||
$params['from'] = $from;
|
||||
}
|
||||
$connect_url = add_query_arg(
|
||||
$params,
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
$this->redirect_to( $connect_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the capital view offer page or overview page with error message.
|
||||
*/
|
||||
public function redirect_to_capital_view_offer_page(): void {
|
||||
$return_url = WC_Payments_Account::get_overview_page_url();
|
||||
$refresh_url = add_query_arg( [ 'wcpay-loan-offer' => '' ], admin_url( 'admin.php' ) );
|
||||
|
||||
try {
|
||||
$request = Get_Account_Capital_Link::create();
|
||||
$type = 'capital_financing_offer';
|
||||
$request->set_type( $type );
|
||||
$request->set_return_url( $return_url );
|
||||
$request->set_refresh_url( $refresh_url );
|
||||
|
||||
$capital_link = $request->send();
|
||||
Tracker::track_admin( 'wcpay_capital_view_offer_redirect' );
|
||||
$this->redirect_to( $capital_link['url'] );
|
||||
} catch ( Exception $e ) {
|
||||
|
||||
$this->redirect_to_overview_page_with_error( [ 'wcpay-loan-offer-error' => '1' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to immediately redirect to the account link.
|
||||
*
|
||||
* @param array $args The arguments to be sent with the link request.
|
||||
*/
|
||||
public function redirect_to_account_link( array $args ): void {
|
||||
try {
|
||||
$link = $this->payments_api_client->get_link( $args );
|
||||
|
||||
if ( isset( $args['type'] ) && 'complete_kyc_link' === $args['type'] && isset( $link['state'] ) ) {
|
||||
set_transient( 'wcpay_stripe_onboarding_state', $link['state'], DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
$this->redirect_to( $link['url'] );
|
||||
} catch ( API_Exception $e ) {
|
||||
$this->redirect_to_overview_page_with_error( [ 'wcpay-server-link-error' => '1' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately redirect to the Connect page.
|
||||
*
|
||||
* Note that this function immediately ends the execution.
|
||||
*
|
||||
* @param string|null $error_message Optional. Error message to show in a notice.
|
||||
* @param string|null $from Optional. Source of the redirect.
|
||||
* @param array $additional_params Optional. Additional URL params to add to the redirect URL.
|
||||
*/
|
||||
public function redirect_to_connect_page( ?string $error_message = null, ?string $from = null, array $additional_params = [] ): void {
|
||||
$params = [
|
||||
'page' => 'wc-admin',
|
||||
'path' => '/payments/connect',
|
||||
];
|
||||
|
||||
if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
// We are already on the Connect page. Do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// If we were given an error message, store it in a very short-lived transient to show it on the page.
|
||||
if ( ! empty( $error_message ) ) {
|
||||
set_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT, $error_message, 30 );
|
||||
}
|
||||
|
||||
$params = array_merge( $params, $additional_params );
|
||||
|
||||
if ( ! empty( $from ) ) {
|
||||
$params['from'] = $from;
|
||||
}
|
||||
|
||||
$this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately redirect to the onboarding wizard.
|
||||
*
|
||||
* Note that this function immediately ends the execution.
|
||||
*
|
||||
* @param string|null $from Optional. Source of the redirect.
|
||||
* @param array $additional_params Optional. Additional URL params to add to the redirect URL.
|
||||
*/
|
||||
public function redirect_to_onboarding_wizard( ?string $from = null, array $additional_params = [] ): void {
|
||||
$params = [
|
||||
'page' => 'wc-admin',
|
||||
'path' => '/payments/onboarding',
|
||||
];
|
||||
|
||||
if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
// We are already in the onboarding wizard. Do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
$params = array_merge( $params, $additional_params );
|
||||
|
||||
if ( ! empty( $from ) ) {
|
||||
$params['from'] = $from;
|
||||
}
|
||||
|
||||
$this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately redirect to the settings page.
|
||||
*
|
||||
* Note that this function immediately ends the execution.
|
||||
*
|
||||
* @param string|null $from Optional. Source of the redirect.
|
||||
* @param array $additional_params Optional. Additional URL params to add to the redirect URL.
|
||||
*/
|
||||
public function redirect_to_settings_page( ?string $from = null, array $additional_params = [] ): void {
|
||||
$params = [
|
||||
'page' => 'wc-settings',
|
||||
'tab' => 'checkout',
|
||||
];
|
||||
|
||||
if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
// We are already in the settings page. Do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
$params = array_merge( $params, $additional_params );
|
||||
|
||||
if ( ! empty( $from ) ) {
|
||||
$params['from'] = $from;
|
||||
}
|
||||
|
||||
$this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the overview page.
|
||||
*
|
||||
* @param string $from Optional. Source of the redirect.
|
||||
* @param array $additional_params Optional. Additional URL params to add to the redirect URL.
|
||||
* */
|
||||
public function redirect_to_overview_page( string $from = '', array $additional_params = [] ): void {
|
||||
$params = $additional_params;
|
||||
if ( '' !== $from ) {
|
||||
$params['from'] = $from;
|
||||
}
|
||||
|
||||
$this->redirect_to( add_query_arg( $params, WC_Payments_Account::get_overview_page_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the overview page with an error message.
|
||||
*
|
||||
* @param array $error The error data to show.
|
||||
*/
|
||||
public function redirect_to_overview_page_with_error( array $error ): void {
|
||||
$overview_url_with_error = add_query_arg(
|
||||
$error,
|
||||
WC_Payments_Account::get_overview_page_url()
|
||||
);
|
||||
$this->redirect_to( $overview_url_with_error );
|
||||
}
|
||||
|
||||
/**
|
||||
* For the connected account, fetches the login url from the API and redirects to it.
|
||||
*/
|
||||
public function redirect_to_login(): void {
|
||||
|
||||
$redirect_url = WC_Payments_Account::get_overview_page_url();
|
||||
|
||||
$request = Get_Account_Login_Data::create();
|
||||
$request->set_redirect_url( $redirect_url );
|
||||
|
||||
$response = $request->send();
|
||||
$login_data = $response->to_array();
|
||||
$this->redirect_to( $login_data['url'] );
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Session_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
|
||||
/**
|
||||
* Handles sessions and session details.
|
||||
*/
|
||||
class WC_Payments_Session_Service {
|
||||
|
||||
const SESSION_STORE_ID_OPTION = 'wcpay_session_store_id';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* Constructor for WC_Payments_Session_Service.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $payments_api_client
|
||||
) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has just logged in.
|
||||
*
|
||||
* The user just logged in if the session cookie contains a different customer ID than the one in the session.
|
||||
*
|
||||
* @return boolean True if the user has just logged in, false in any other case.
|
||||
*/
|
||||
public function user_just_logged_in(): bool {
|
||||
if ( ! get_current_user_id() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WC()->initialize_session();
|
||||
$session_handler = WC()->session;
|
||||
// The Store API SessionHandler (used by WooPay) doesn't provide this method.
|
||||
if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) {
|
||||
return false;
|
||||
}
|
||||
$cookie = $session_handler->get_session_cookie();
|
||||
if ( ! $cookie ) {
|
||||
return false;
|
||||
}
|
||||
$cookie_customer_id = $cookie[0];
|
||||
|
||||
return $session_handler->get_customer_id() !== $cookie_customer_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Sift session ID for the current browsing session.
|
||||
*
|
||||
* @return string|null The Sift session ID or null if it can't be determined.
|
||||
*/
|
||||
public function get_sift_session_id(): ?string {
|
||||
if ( $this->user_just_logged_in() ) {
|
||||
return $this->get_cookie_session_id();
|
||||
}
|
||||
|
||||
if ( is_a( WC()->session, 'WC_Session' ) ) {
|
||||
return $this->generate_session_id( $this->get_store_id(), (string) WC()->session->get_customer_id() );
|
||||
}
|
||||
|
||||
return null; // We do not have a valid session for the current process.
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a customer with the current browsing session, for tracking purposes.
|
||||
*
|
||||
* @param string $customer_id Stripe customer ID.
|
||||
*
|
||||
* @return array An array, containing a `success` flag.
|
||||
*
|
||||
* @throws API_Exception If an error occurs.
|
||||
*/
|
||||
public function link_current_session_to_customer( string $customer_id ): array {
|
||||
return $this->payments_api_client->link_session_to_customer( $this->get_sift_session_id(), $customer_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session ID used until now for the current browsing session.
|
||||
*
|
||||
* It looks for the logged in user ID stored in the session cookie, and uses that to generate a session ID.
|
||||
*
|
||||
* @return string|null Session ID, or null if unknown.
|
||||
*/
|
||||
public function get_cookie_session_id(): ?string {
|
||||
$session_handler = WC()->session;
|
||||
if ( ! $session_handler ) {
|
||||
return null;
|
||||
}
|
||||
// The Store API SessionHandler (used by WooPay) doesn't provide this method.
|
||||
if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cookie = $session_handler->get_session_cookie();
|
||||
if ( ! $cookie ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cookie_customer_id = $cookie[0];
|
||||
|
||||
return $this->generate_session_id( $this->get_store_id(), (string) $cookie_customer_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store ID for use in sessions.
|
||||
*
|
||||
* This is used to consistently identify the store in WooPayments sessions.
|
||||
* If it doesn't exist, it is generated randomly and stored in the database.
|
||||
*
|
||||
* @return string The store ID or empty string if it can't be determined.
|
||||
*/
|
||||
public function get_store_id(): string {
|
||||
// We will use a stored random store ID.
|
||||
$store_id = get_option( self::SESSION_STORE_ID_OPTION, false );
|
||||
if ( ! $store_id ) {
|
||||
$store_id = $this->generate_store_id();
|
||||
update_option( self::SESSION_STORE_ID_OPTION, $store_id );
|
||||
}
|
||||
|
||||
return $store_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random store ID.
|
||||
*
|
||||
* The generated ID is case-sensitive and contains 32 characters.
|
||||
*
|
||||
* @return string The generated store ID.
|
||||
*/
|
||||
private function generate_store_id(): string {
|
||||
// Prefix it with 'st_' (from store) to make it easier to identify.
|
||||
$prefix = 'st_';
|
||||
|
||||
// We will generate 32 characters in total, including the prefix length.
|
||||
$length = 32 - strlen( $prefix );
|
||||
|
||||
// We will use alphanumerical characters.
|
||||
$include_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
// Add some special characters, not all. Play it safe.
|
||||
// See the Sift restrictions for $user_id.
|
||||
// @link https://sift.com/developers/docs/curl/events-api/fields.
|
||||
$include_chars .= '-$:.^!';
|
||||
// Finally, shuffle them for extra randomness.
|
||||
$include_chars = str_shuffle( $include_chars );
|
||||
|
||||
$char_length = strlen( $include_chars );
|
||||
$random_string = '';
|
||||
for ( $i = 0; $i < $length; $i++ ) {
|
||||
$random_string .= $include_chars [ wp_rand( 0, $char_length - 1 ) ];
|
||||
}
|
||||
|
||||
return $prefix . $random_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session ID based on the store ID and the user ID.
|
||||
*
|
||||
* @param string $store_id The session store ID.
|
||||
* @param string $user_id The user ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate_session_id( string $store_id, string $user_id ): string {
|
||||
return $store_id . '_' . $user_id;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments WC_Payments_Settings_Service Class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencySettingsInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC_Payments_Settings_Service.
|
||||
*/
|
||||
class WC_Payments_Settings_Service implements MultiCurrencySettingsInterface {
|
||||
/**
|
||||
* Checks if dev mode is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_dev_mode(): bool {
|
||||
return WC_Payments::mode()->is_dev();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin file path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_plugin_file_path(): string {
|
||||
return WCPAY_PLUGIN_FILE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_plugin_version(): string {
|
||||
return WCPAY_VERSION_NUMBER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Status class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into Woo Status pages to provide extra tooling and information about WCPay.
|
||||
*/
|
||||
class WC_Payments_Status {
|
||||
/**
|
||||
* Instance of WC_Payment_Gateway_WCPay
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* Instance of WC_Payments_Http_Interface
|
||||
*
|
||||
* @var WC_Payments_Http_Interface
|
||||
*/
|
||||
private $http;
|
||||
|
||||
/**
|
||||
* Instance of WC_Payments_Account
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC_Payments_Status constructor.
|
||||
*
|
||||
* @param WC_Payment_Gateway_WCPay $gateway The main gateway instance.
|
||||
* @param WC_Payments_Http_Interface $http A class implementing WC_Payments_Http_Interface.
|
||||
* @param WC_Payments_Account $account The account service.
|
||||
*/
|
||||
public function __construct( $gateway, $http, $account ) {
|
||||
$this->gateway = $gateway;
|
||||
$this->http = $http;
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class's WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'woocommerce_system_status_report', [ $this, 'render_status_report_section' ], 1 );
|
||||
add_filter( 'woocommerce_debug_tools', [ $this, 'debug_tools' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WCPay tools to the Woo debug tools.
|
||||
*
|
||||
* @param array $tools List of current available tools.
|
||||
*/
|
||||
public function debug_tools( $tools ) {
|
||||
$tools['clear_wcpay_account_cache'] = [
|
||||
'name' => sprintf(
|
||||
/* translators: %s: WooPayments */
|
||||
__( 'Clear %s account cache', 'woocommerce-payments' ),
|
||||
'WooPayments'
|
||||
),
|
||||
'button' => __( 'Clear', 'woocommerce-payments' ),
|
||||
'desc' => sprintf(
|
||||
/* translators: %s: WooPayments */
|
||||
__( 'This tool will clear the account cached values used in %s.', 'woocommerce-payments' ),
|
||||
'WooPayments'
|
||||
),
|
||||
'callback' => [ $this->account, 'refresh_account_data' ],
|
||||
];
|
||||
return $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders WCPay information on the status page.
|
||||
*/
|
||||
public function render_status_report_section() {
|
||||
?>
|
||||
<table class="wc_status_table widefat" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3" data-export-label="WooPayments">
|
||||
<h2>WooPayments</h2>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-export-label="Version"><?php esc_html_e( 'Version', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help">
|
||||
<?php
|
||||
/* translators: %s: WooPayments */
|
||||
echo wc_help_tip( sprintf( esc_html__( 'The current version of the %s extension.', 'woocommerce-payments' ), 'WooPayments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo esc_html( WCPAY_VERSION_NUMBER ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Connected to WPCOM"><?php esc_html_e( 'Connected to WPCOM', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help">
|
||||
<?php
|
||||
/* translators: %s: WooPayments */
|
||||
echo wc_help_tip( sprintf( esc_html__( 'Can your store connect securely to wordpress.com? Without a proper WPCOM connection %s can\'t function!', 'woocommerce-payments' ), 'WooPayments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo $this->http->is_connected() ? esc_html__( 'Yes', 'woocommerce-payments' ) : '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'No', 'woocommerce-payments' ) . '</mark>'; ?></td>
|
||||
</tr>
|
||||
<?php if ( $this->http->is_connected() ) : ?>
|
||||
<tr>
|
||||
<td data-export-label="WPCOM Blog ID"><?php esc_html_e( 'WPCOM Blog ID', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'The corresponding wordpress.com blog ID for this store.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo esc_html( $this->http->is_connected() ? $this->http->get_blog_id() : '-' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Account ID"><?php esc_html_e( 'Account ID', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'The merchant account ID you are currently using to process payments with.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo $this->gateway->is_connected() ? esc_html( $this->account->get_stripe_account_id() ?? '-' ) : '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not connected', 'woocommerce-payments' ) . '</mark>'; ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
// Only display the rest if the payment gateway is connected since many places check for this and we might get inaccurate data.
|
||||
if ( $this->gateway->is_connected() ) :
|
||||
?>
|
||||
<tr>
|
||||
<td data-export-label="Payment Gateway"><?php esc_html_e( 'Payment Gateway', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Is the payment gateway ready and enabled for use on your store?', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo $this->gateway->needs_setup() ? '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Needs setup', 'woocommerce-payments' ) . '</mark>' : ( $this->gateway->is_enabled() ? esc_html__( 'Enabled', 'woocommerce-payments' ) : esc_html__( 'Disabled', 'woocommerce-payments' ) ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Test Mode"><?php esc_html_e( 'Test Mode', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the payment gateway has test payments enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php WC_Payments::mode()->is_test() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Enabled APMs"><?php esc_html_e( 'Enabled APMs', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'What payment methods are enabled for the store.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo esc_html( implode( ',', $this->gateway->get_upe_enabled_payment_method_ids() ) ); ?></td>
|
||||
</tr>
|
||||
|
||||
<?php if ( ! WC_Payments_Features::is_woopay_express_checkout_enabled() ) : ?>
|
||||
<tr>
|
||||
<td data-export-label="WooPay"><?php esc_html_e( 'WooPay Express Checkout', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help">
|
||||
<?php
|
||||
/* translators: %s: WooPayments */
|
||||
echo wc_help_tip( sprintf( esc_html__( 'WooPay is not available, as a %s feature, or the store is not yet eligible.', 'woocommerce-payments' ), 'WooPayments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo ! WC_Payments_Features::is_woopay_eligible() ? esc_html__( 'Not eligible', 'woocommerce-payments' ) : esc_html__( 'Not active', 'woocommerce-payments' ); ?></td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<tr>
|
||||
<td data-export-label="WooPay"><?php esc_html_e( 'WooPay Express Checkout', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the new WooPay Express Checkout is enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$woopay_enabled_locations = $this->gateway->get_option( 'platform_checkout_button_locations', [] );
|
||||
$woopay_enabled_locations = empty( $woopay_enabled_locations ) ? 'no locations enabled' : implode( ',', $woopay_enabled_locations );
|
||||
echo esc_html( WC_Payments_Features::is_woopay_enabled() ? __( 'Enabled', 'woocommerce-payments' ) . ' (' . $woopay_enabled_locations . ')' : __( 'Disabled', 'woocommerce-payments' ) );
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="WooPay Incompatible Extensions"><?php esc_html_e( 'WooPay Incompatible Extensions', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether there are extensions active that are have known incompatibilities with the functioning of the new WooPay Express Checkout.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php get_option( \WCPay\WooPay\WooPay_Scheduler::INVALID_EXTENSIONS_FOUND_OPTION_NAME, false ) ? esc_html_e( 'Yes', 'woocommerce-payments' ) : esc_html_e( 'No', 'woocommerce-payments' ); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
<tr>
|
||||
<td data-export-label="Apple Pay / Google Pay"><?php esc_html_e( 'Apple Pay / Google Pay Express Checkout', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the store has Payment Request enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$payment_request_enabled = 'yes' === $this->gateway->get_option( 'payment_request' );
|
||||
$payment_request_enabled_locations = $this->gateway->get_option( 'payment_request_button_locations', [] );
|
||||
$payment_request_enabled_locations = empty( $payment_request_enabled_locations ) ? 'no locations enabled' : implode( ',', $payment_request_enabled_locations );
|
||||
echo esc_html( $payment_request_enabled ? __( 'Enabled', 'woocommerce-payments' ) . ' (' . $payment_request_enabled_locations . ')' : __( 'Disabled', 'woocommerce-payments' ) );
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Fraud Protection Level"><?php esc_html_e( 'Fraud Protection Level', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'The current fraud protection level the payment gateway is using.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo esc_html( $this->gateway->get_option( 'current_protection_level' ) ); ?></td>
|
||||
</tr>
|
||||
<?php if ( $this->gateway->get_option( 'current_protection_level' ) === 'advanced' ) : ?>
|
||||
<tr>
|
||||
<td data-export-label="Enabled Fraud Filters"><?php esc_html_e( 'Enabled Fraud Filters', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'The advanced fraud protection filters currently enabled.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td>
|
||||
<?php
|
||||
// Process the list.
|
||||
$adv_fraud_settings = json_decode( wp_json_encode( $this->gateway->get_option( 'advanced_fraud_protection_settings' ) ), true );
|
||||
$list = array_filter(
|
||||
array_map(
|
||||
function ( $rule ) {
|
||||
if ( empty( $rule['key'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch ( $rule['key'] ) {
|
||||
case 'avs_verification':
|
||||
return 'AVS Verification';
|
||||
case 'international_ip_address':
|
||||
return 'International IP Address';
|
||||
case 'ip_address_mismatch':
|
||||
return 'IP Address Mismatch';
|
||||
case 'address_mismatch':
|
||||
return 'Address Mismatch';
|
||||
case 'purchase_price_threshold':
|
||||
return 'Purchase Price Threshold';
|
||||
case 'order_items_threshold':
|
||||
return 'Order Items Threshold';
|
||||
default:
|
||||
// Ignore all others.
|
||||
return null;
|
||||
}
|
||||
},
|
||||
$adv_fraud_settings
|
||||
)
|
||||
);
|
||||
|
||||
echo empty( $list ) ? '-' : esc_html( implode( ',', $list ) );
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
<tr>
|
||||
<td data-export-label="Multi-currency"><?php esc_html_e( 'Multi-currency', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the store has the Multi-currency feature enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php WC_Payments_Features::is_customer_multi_currency_enabled() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Auth and Capture"><?php esc_html_e( 'Auth and Capture', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the store has the Auth & Capture feature enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$manual_capture_enabled = 'yes' === $this->gateway->get_option( 'manual_capture' );
|
||||
echo $manual_capture_enabled ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' );
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Documents"><?php esc_html_e( 'Documents', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the tax documents section is enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php WC_Payments_Features::is_documents_section_enabled() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td>
|
||||
</tr>
|
||||
<?php endif; // Gateway connected. ?>
|
||||
<?php endif; // Connected to WPCOM. ?>
|
||||
<tr>
|
||||
<td data-export-label="Logging"><?php esc_html_e( 'Logging', 'woocommerce-payments' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether debug logging is enabled and working or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php \WCPay\Logger::can_log() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Tasks class
|
||||
*
|
||||
* @package WooCommerce\Payments\Tasks
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
|
||||
use WooCommerce\Payments\Tasks\WC_Payments_Task_Disputes;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into WC TaskLists to display WCPay tasks.
|
||||
*/
|
||||
class WC_Payments_Tasks {
|
||||
|
||||
/**
|
||||
* WC_Payments_Admin_Tasks constructor.
|
||||
*/
|
||||
public static function init() {
|
||||
// As WooCommerce Onboarding tasks need to hook into 'init' and requires an API call.
|
||||
// We only add this task for users who can manage_woocommerce / view the task.
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'init', [ __CLASS__, 'add_task_disputes_need_response' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task to the WC 'Things to do next' task list the if disputes awaiting response.
|
||||
*/
|
||||
public static function add_task_disputes_need_response() {
|
||||
$account_service = WC_Payments::get_account_service();
|
||||
// The task is not required if the account is not connected, under review, or rejected.
|
||||
if ( ! $account_service || ! $account_service->is_stripe_account_valid() || $account_service->is_account_under_review() || $account_service->is_account_rejected() ) {
|
||||
return;
|
||||
}
|
||||
include_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php';
|
||||
|
||||
// 'extended' = 'Things to do next' task list on WooCommerce > Home.
|
||||
TaskLists::add_task( 'extended', new WC_Payments_Task_Disputes() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Token_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WCPay\Logger;
|
||||
use WCPay\Payment_Methods\CC_Payment_Gateway;
|
||||
use WCPay\Constants\Payment_Method;
|
||||
|
||||
/**
|
||||
* Handles and process WC payment tokens API.
|
||||
* Seen in checkout page and my account->add payment method page.
|
||||
*/
|
||||
class WC_Payments_Token_Service {
|
||||
const REUSABLE_GATEWAYS_BY_PAYMENT_METHOD = [
|
||||
Payment_Method::CARD => WC_Payment_Gateway_WCPay::GATEWAY_ID,
|
||||
Payment_Method::SEPA => WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA,
|
||||
Payment_Method::LINK => WC_Payment_Gateway_WCPay::GATEWAY_ID,
|
||||
];
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* WC_Payments_Customer instance for working with customer information
|
||||
*
|
||||
* @var WC_Payments_Customer_Service
|
||||
*/
|
||||
private $customer_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Token_Service constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client Payments API client.
|
||||
* @param WC_Payments_Customer_Service $customer_service Customer class instance.
|
||||
*/
|
||||
public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Customer_Service $customer_service ) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
$this->customer_service = $customer_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes hooks.
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
|
||||
add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ], 10, 2 );
|
||||
add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
|
||||
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item_sepa' ], 10, 2 );
|
||||
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item_link' ], 10, 2 );
|
||||
add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_sepa_label' ] );
|
||||
add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_stripe_link_label' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and add a token to an user, based on the payment_method object
|
||||
*
|
||||
* @param array $payment_method Payment method to be added.
|
||||
* @param WP_User $user User to attach payment method to.
|
||||
* @return WC_Payment_Token The WC object for the payment token.
|
||||
*/
|
||||
public function add_token_to_user( $payment_method, $user ) {
|
||||
// Clear cached payment methods.
|
||||
$this->customer_service->clear_cached_payment_methods_for_user( $user->ID );
|
||||
|
||||
switch ( $payment_method['type'] ) {
|
||||
case Payment_Method::SEPA:
|
||||
$token = new WC_Payment_Token_WCPay_SEPA();
|
||||
$gateway_id = WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA;
|
||||
$token->set_gateway_id( $gateway_id );
|
||||
$token->set_last4( $payment_method[ Payment_Method::SEPA ]['last4'] );
|
||||
break;
|
||||
case Payment_Method::LINK:
|
||||
$token = new WC_Payment_Token_WCPay_Link();
|
||||
$gateway_id = CC_Payment_Gateway::GATEWAY_ID;
|
||||
$token->set_gateway_id( $gateway_id );
|
||||
$token->set_email( $payment_method[ Payment_Method::LINK ]['email'] );
|
||||
break;
|
||||
case Payment_Method::CARD_PRESENT:
|
||||
$token = new WC_Payment_Token_CC();
|
||||
$token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID );
|
||||
$token->set_expiry_month( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_month'] );
|
||||
$token->set_expiry_year( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_year'] );
|
||||
$token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD_PRESENT ]['brand'] ) );
|
||||
$token->set_last4( $payment_method[ Payment_Method::CARD_PRESENT ]['last4'] );
|
||||
break;
|
||||
default:
|
||||
$token = new WC_Payment_Token_CC();
|
||||
$token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID );
|
||||
$token->set_expiry_month( $payment_method[ Payment_Method::CARD ]['exp_month'] );
|
||||
$token->set_expiry_year( $payment_method[ Payment_Method::CARD ]['exp_year'] );
|
||||
$token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD ]['display_brand'] ?? $payment_method[ Payment_Method::CARD ]['networks']['preferred'] ?? $payment_method[ Payment_Method::CARD ]['brand'] ) );
|
||||
$token->set_last4( $payment_method[ Payment_Method::CARD ]['last4'] );
|
||||
|
||||
}
|
||||
$token->set_token( $payment_method['id'] );
|
||||
$token->set_user_id( $user->ID );
|
||||
$token->save();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a payment method to a user.
|
||||
*
|
||||
* @param string $payment_method_id Payment method to be added.
|
||||
* @param WP_User $user User to attach payment method to.
|
||||
* @return WC_Payment_Token_CC The newly created token.
|
||||
*/
|
||||
public function add_payment_method_to_user( $payment_method_id, $user ) {
|
||||
$payment_method_object = $this->payments_api_client->get_payment_method( $payment_method_id );
|
||||
return $this->add_token_to_user( $payment_method_object, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean value if payment method type matches relevant payment gateway.
|
||||
*
|
||||
* @param string $payment_method_type Stripe payment method type ID.
|
||||
* @param string $gateway_id WC payment gateway ID.
|
||||
* @return bool True, if payment method type matches gateway, false if otherwise.
|
||||
*/
|
||||
public function is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id ) {
|
||||
return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets saved tokens from API if they don't already exist in WooCommerce.
|
||||
*
|
||||
* @param array $tokens Array of tokens.
|
||||
* @param string $user_id WC user ID.
|
||||
* @param string $gateway_id WC gateway ID.
|
||||
* @return array
|
||||
*/
|
||||
public function woocommerce_get_customer_payment_tokens( $tokens, $user_id, $gateway_id ) {
|
||||
|
||||
if ( ( ! empty( $gateway_id ) && ! in_array( $gateway_id, self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) || ! is_user_logged_in() ) {
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
if ( count( $tokens ) >= get_option( 'posts_per_page' ) ) {
|
||||
// The tokens data store is not paginated and only the first "post_per_page" (defaults to 10) tokens are retrieved.
|
||||
// Having 10 saved credit cards is considered an unsupported edge case, new ones that have been stored in Stripe won't be added.
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
try {
|
||||
$customer_id = $this->customer_service->get_customer_id_by_user_id( $user_id );
|
||||
|
||||
if ( null === $customer_id ) {
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
$stored_tokens = [];
|
||||
|
||||
foreach ( $tokens as $token ) {
|
||||
if ( in_array( $token->get_gateway_id(), self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) {
|
||||
$stored_tokens[ $token->get_token() ] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
$retrievable_payment_method_types = $this->get_retrievable_payment_method_types( $gateway_id );
|
||||
|
||||
$payment_methods = [];
|
||||
|
||||
foreach ( $retrievable_payment_method_types as $type ) {
|
||||
$payment_methods[] = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type );
|
||||
}
|
||||
|
||||
$payment_methods = array_merge( ...$payment_methods );
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( 'Failed to fetch payment methods for customer.' . $e );
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
// Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases.
|
||||
remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
|
||||
|
||||
foreach ( $payment_methods as $payment_method ) {
|
||||
if ( ! isset( $payment_method['type'] ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! isset( $stored_tokens[ $payment_method['id'] ] ) && ( $this->is_valid_payment_method_type_for_gateway( $payment_method['type'], $gateway_id ) || empty( $gateway_id ) ) ) {
|
||||
$token = $this->add_token_to_user( $payment_method, get_user_by( 'id', $user_id ) );
|
||||
$tokens[ $token->get_id() ] = $token;
|
||||
} else {
|
||||
unset( $stored_tokens[ $payment_method['id'] ] );
|
||||
}
|
||||
}
|
||||
add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
|
||||
|
||||
// Remove the payment methods that no longer exist in Stripe's side.
|
||||
remove_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
|
||||
foreach ( $stored_tokens as $token ) {
|
||||
unset( $tokens[ $token->get_id() ] );
|
||||
$token->delete();
|
||||
}
|
||||
add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the payment method types for which tokens should be retrieved.
|
||||
*
|
||||
* This function determines the appropriate payment method types based on the provided gateway ID.
|
||||
* - If a gateway ID is provided, it retrieves the payment methods specific to that gateway to prevent duplication of saved tokens under incorrect payment methods during checkout.
|
||||
* - If no gateway ID is provided, it retrieves the default payment methods to fetch all saved tokens, e.g., for the Blocks checkout or My Account page.
|
||||
*
|
||||
* @param string|null $gateway_id The optional ID of the gateway.
|
||||
* @return array The list of retrievable payment method types.
|
||||
*/
|
||||
private function get_retrievable_payment_method_types( $gateway_id = null ) {
|
||||
if ( empty( $gateway_id ) ) {
|
||||
return $this->get_all_retrievable_payment_types();
|
||||
} else {
|
||||
return $this->get_gateway_specific_retrievable_payment_types( $gateway_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the enabled retrievable payment method types.
|
||||
*
|
||||
* @return array Enabled retrievable payment method types.
|
||||
*/
|
||||
private function get_all_retrievable_payment_types() {
|
||||
$types = [ Payment_Method::CARD ];
|
||||
|
||||
if ( $this->is_payment_method_enabled( Payment_Method::SEPA ) ) {
|
||||
$types[] = Payment_Method::SEPA;
|
||||
}
|
||||
|
||||
if ( $this->is_payment_method_enabled( Payment_Method::LINK ) ) {
|
||||
$types[] = Payment_Method::LINK;
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
/**
|
||||
* Returns retrievable payment method types for a given gateway.
|
||||
*
|
||||
* @param string $gateway_id The ID of the gateway.
|
||||
* @return array Retrievable payment method types for the specified gateway.
|
||||
*/
|
||||
private function get_gateway_specific_retrievable_payment_types( $gateway_id ) {
|
||||
$types = [];
|
||||
|
||||
foreach ( self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method => $gateway ) {
|
||||
if ( $gateway !== $gateway_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stripe Link is part of the card gateway, so we need to check separately if Link is enabled.
|
||||
if ( Payment_Method::LINK === $payment_method && ! $this->is_payment_method_enabled( Payment_Method::LINK ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$types[] = $payment_method;
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a payment method is enabled.
|
||||
*
|
||||
* @param string $payment_method The payment method to check.
|
||||
* @return bool True if the payment method is enabled, false otherwise.
|
||||
*/
|
||||
private function is_payment_method_enabled( $payment_method ) {
|
||||
return in_array( $payment_method, WC_Payments::get_gateway()->get_upe_enabled_payment_method_ids(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete token from Stripe.
|
||||
*
|
||||
* @param string $token_id Token ID.
|
||||
* @param WC_Payment_Token $token Token object.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function woocommerce_payment_token_deleted( $token_id, $token ) {
|
||||
|
||||
// If it's not reusable payment method, we don't need to perform any additional checks.
|
||||
if ( ! in_array( $token->get_gateway_id(), self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) {
|
||||
return;
|
||||
}
|
||||
// First check if it's live mode.
|
||||
// Second check if it's admin.
|
||||
// Third check if it's not production environment.
|
||||
// When all conditions are met, we don't want to delete the payment method from Stripe.
|
||||
// This is to avoid detaching the payment method from the live stripe account on non production environments.
|
||||
if (
|
||||
WC_Payments::mode()->is_live() &&
|
||||
is_admin() &&
|
||||
'production' !== wp_get_environment_type()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->payments_api_client->detach_payment_method( $token->get_token() );
|
||||
// Clear cached payment methods.
|
||||
$this->customer_service->clear_cached_payment_methods_for_user( $token->get_user_id() );
|
||||
} catch ( Exception $e ) {
|
||||
Logger::log( 'Error detaching payment method:' . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as default in Stripe.
|
||||
*
|
||||
* @param string $token_id Token ID.
|
||||
* @param WC_Payment_Token $token Token object.
|
||||
*/
|
||||
public function woocommerce_payment_token_set_default( $token_id, $token ) {
|
||||
|
||||
if ( in_array( $token->get_gateway_id(), self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) {
|
||||
$customer_id = $this->customer_service->get_customer_id_by_user_id( $token->get_user_id() );
|
||||
if ( $customer_id ) {
|
||||
$this->customer_service->set_default_payment_method_for_customer( $customer_id, $token->get_token() );
|
||||
// Clear cached payment methods.
|
||||
$this->customer_service->clear_cached_payment_methods_for_user( $token->get_user_id() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the output for SEPA on the my account page.
|
||||
*
|
||||
* @param array $item Individual list item from woocommerce_saved_payment_methods_list.
|
||||
* @param WC_Payment_Token|WC_Payment_Token_WCPay_SEPA $payment_token The payment token associated with this method entry.
|
||||
* @return array Filtered item
|
||||
*/
|
||||
public function get_account_saved_payment_methods_list_item_sepa( $item, $payment_token ) {
|
||||
if ( WC_Payment_Token_WCPay_SEPA::TYPE === strtolower( $payment_token->get_type() ) ) {
|
||||
$item['method']['last4'] = $payment_token->get_last4();
|
||||
$item['method']['brand'] = esc_html__( 'SEPA IBAN', 'woocommerce-payments' );
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the output for Stripe Link on the My account page.
|
||||
*
|
||||
* @param array $item Individual list item from woocommerce_saved_payment_methods_list.
|
||||
* @param WC_Payment_Token|WC_Payment_Token_WCPay_Link $payment_token The payment token associated with this method entry.
|
||||
* @return array Filtered item
|
||||
*/
|
||||
public function get_account_saved_payment_methods_list_item_link( $item, $payment_token ) {
|
||||
if ( WC_Payment_Token_WCPay_Link::TYPE === strtolower( $payment_token->get_type() ) ) {
|
||||
$item['method']['last4'] = $payment_token->get_redacted_email();
|
||||
$item['method']['brand'] = esc_html__( 'Stripe Link email', 'woocommerce-payments' );
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the SEPA IBAN label on My Account page.
|
||||
*
|
||||
* @param string $label Token label.
|
||||
* @return string $label Capitalized SEPA IBAN label.
|
||||
*/
|
||||
public function normalize_sepa_label( $label ) {
|
||||
if ( 'sepa iban' === strtolower( $label ) ) {
|
||||
return __( 'SEPA IBAN', 'woocommerce-payments' );
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the Stripe Link label on My Account page.
|
||||
*
|
||||
* @param string $label Token label.
|
||||
* @return string $label Capitalized SEPA IBAN label.
|
||||
*/
|
||||
public function normalize_stripe_link_label( $label ) {
|
||||
if ( 'stripe link email' === strtolower( $label ) ) {
|
||||
return 'Stripe Link email';
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+874
@@ -0,0 +1,874 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Webhook_Processing_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Constants\Order_Status;
|
||||
use WCPay\Constants\Payment_Method;
|
||||
use WCPay\Core\Server\Request\Get_Intention;
|
||||
use WCPay\Database_Cache;
|
||||
use WCPay\Exceptions\Invalid_Payment_Method_Exception;
|
||||
use WCPay\Exceptions\Invalid_Webhook_Data_Exception;
|
||||
use WCPay\Exceptions\Order_Not_Found_Exception;
|
||||
use WCPay\Exceptions\Rest_Request_Exception;
|
||||
use WCPay\Logger;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to process webhook data.
|
||||
*/
|
||||
class WC_Payments_Webhook_Processing_Service {
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
protected $api_client;
|
||||
|
||||
/**
|
||||
* DB wrapper.
|
||||
*
|
||||
* @var WC_Payments_DB
|
||||
*/
|
||||
private $wcpay_db;
|
||||
|
||||
/**
|
||||
* WC Payments Account.
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC Payments Remote Note Service.
|
||||
*
|
||||
* @var WC_Payments_Remote_Note_Service
|
||||
*/
|
||||
private $remote_note_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Order_Service instance
|
||||
*
|
||||
* @var WC_Payments_Order_Service
|
||||
*/
|
||||
protected $order_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_In_Person_Payments_Receipts_Service
|
||||
*
|
||||
* @var WC_Payments_In_Person_Payments_Receipts_Service
|
||||
*/
|
||||
private $receipt_service;
|
||||
|
||||
/**
|
||||
* WC_Payment_Gateway_WCPay
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $wcpay_gateway;
|
||||
|
||||
/**
|
||||
* WC_Payment_Gateway_WCPay
|
||||
*
|
||||
* @var WC_Payments_Customer_Service
|
||||
*/
|
||||
private $customer_service;
|
||||
|
||||
/**
|
||||
* Database_Cache instance.
|
||||
*
|
||||
* @var Database_Cache
|
||||
*/
|
||||
private $database_cache;
|
||||
|
||||
/**
|
||||
* WC_Payments_Webhook_Processing_Service constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
|
||||
* @param WC_Payments_DB $wcpay_db WC_Payments_DB instance.
|
||||
* @param WC_Payments_Account $account WC_Payments_Account instance.
|
||||
* @param WC_Payments_Remote_Note_Service $remote_note_service WC_Payments_Remote_Note_Service instance.
|
||||
* @param WC_Payments_Order_Service $order_service WC_Payments_Order_Service instance.
|
||||
* @param WC_Payments_In_Person_Payments_Receipts_Service $receipt_service WC_Payments_In_Person_Payments_Receipts_Service instance.
|
||||
* @param WC_Payment_Gateway_WCPay $wcpay_gateway WC_Payment_Gateway_WCPay instance.
|
||||
* @param WC_Payments_Customer_Service $customer_service WC_Payments_Customer_Service instance.
|
||||
* @param Database_Cache $database_cache Database_Cache instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $api_client,
|
||||
WC_Payments_DB $wcpay_db,
|
||||
WC_Payments_Account $account,
|
||||
WC_Payments_Remote_Note_Service $remote_note_service,
|
||||
WC_Payments_Order_Service $order_service,
|
||||
WC_Payments_In_Person_Payments_Receipts_Service $receipt_service,
|
||||
WC_Payment_Gateway_WCPay $wcpay_gateway,
|
||||
WC_Payments_Customer_Service $customer_service,
|
||||
Database_Cache $database_cache
|
||||
) {
|
||||
$this->wcpay_db = $wcpay_db;
|
||||
$this->account = $account;
|
||||
$this->remote_note_service = $remote_note_service;
|
||||
$this->order_service = $order_service;
|
||||
$this->api_client = $api_client;
|
||||
$this->receipt_service = $receipt_service;
|
||||
$this->wcpay_gateway = $wcpay_gateway;
|
||||
$this->customer_service = $customer_service;
|
||||
$this->database_cache = $database_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook event data.
|
||||
*
|
||||
* @param array $event_body Body data of webhook request.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception
|
||||
*/
|
||||
public function process( array $event_body ) {
|
||||
// Extract information about the webhook event.
|
||||
$event_type = $this->read_webhook_property( $event_body, 'type' );
|
||||
|
||||
Logger::debug( 'Webhook received: ' . $event_type );
|
||||
Logger::debug(
|
||||
'Webhook body: '
|
||||
. var_export( WC_Payments_Utils::redact_array( $event_body, WC_Payments_API_Client::API_KEYS_TO_REDACT ), true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
|
||||
);
|
||||
|
||||
if ( $this->is_webhook_mode_mismatch( $event_body ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
do_action( 'woocommerce_payments_before_webhook_delivery', $event_type, $event_body );
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( $e );
|
||||
}
|
||||
|
||||
switch ( $event_type ) {
|
||||
case 'charge.refunded':
|
||||
$this->process_webhook_refund_triggered_externally( $event_body );
|
||||
break;
|
||||
case 'charge.refund.updated':
|
||||
$this->process_webhook_refund_updated( $event_body );
|
||||
break;
|
||||
case 'charge.dispute.created':
|
||||
$this->process_webhook_dispute_created( $event_body );
|
||||
break;
|
||||
case 'charge.dispute.closed':
|
||||
$this->process_webhook_dispute_closed( $event_body );
|
||||
break;
|
||||
case 'charge.dispute.funds_reinstated':
|
||||
case 'charge.dispute.funds_withdrawn':
|
||||
case 'charge.dispute.updated':
|
||||
$this->process_webhook_dispute_updated( $event_body );
|
||||
break;
|
||||
case 'charge.expired':
|
||||
$this->process_webhook_expired_authorization( $event_body );
|
||||
break;
|
||||
case 'account.updated':
|
||||
$this->account->refresh_account_data();
|
||||
$this->customer_service->delete_cached_payment_methods();
|
||||
break;
|
||||
case 'wcpay.notification':
|
||||
$this->process_wcpay_notification( $event_body );
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
$this->process_webhook_payment_intent_failed( $event_body );
|
||||
break;
|
||||
case 'payment_intent.succeeded':
|
||||
$this->process_webhook_payment_intent_succeeded( $event_body );
|
||||
break;
|
||||
case 'payment_intent.canceled':
|
||||
$this->process_webhook_payment_intent_canceled( $event_body );
|
||||
break;
|
||||
case 'payment_intent.amount_capturable_updated':
|
||||
$this->process_webhook_payment_intent_amount_capturable_updated( $event_body );
|
||||
break;
|
||||
case 'invoice.upcoming':
|
||||
WC_Payments_Subscriptions::get_event_handler()->handle_invoice_upcoming( $event_body );
|
||||
break;
|
||||
case 'invoice.paid':
|
||||
WC_Payments_Subscriptions::get_event_handler()->handle_invoice_paid( $event_body );
|
||||
break;
|
||||
case 'invoice.payment_failed':
|
||||
WC_Payments_Subscriptions::get_event_handler()->handle_invoice_payment_failed( $event_body );
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
do_action( 'woocommerce_payments_after_webhook_delivery', $event_type, $event_body );
|
||||
} catch ( Exception $e ) {
|
||||
Logger::error( $e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check webhook mode against the gateway mode.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @return bool Indicates whether the event's mode is different from the gateway's mode
|
||||
* @throws Invalid_Webhook_Data_Exception Event mode does not match the gateway mode.
|
||||
*/
|
||||
private function is_webhook_mode_mismatch( array $event_body ): bool {
|
||||
if ( ! $this->has_webhook_property( $event_body, 'livemode' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_gateway_live_mode = WC_Payments::mode()->is_live();
|
||||
$is_event_live_mode = $this->read_webhook_property( $event_body, 'livemode' );
|
||||
|
||||
if ( $is_gateway_live_mode !== $is_event_live_mode ) {
|
||||
$event_id = $this->read_webhook_property( $event_body, 'id' );
|
||||
|
||||
Logger::error(
|
||||
sprintf(
|
||||
'Webhook event mode did not match the gateway mode (event ID: %s)',
|
||||
$event_id
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook refund updated.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
* @throws Invalid_Payment_Method_Exception When unable to resolve charge ID to order.
|
||||
*/
|
||||
private function process_webhook_refund_updated( $event_body ) {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
|
||||
// First, check the reason for the update. We're only interested in a status of failed.
|
||||
$status = $this->read_webhook_property( $event_object, 'status' );
|
||||
if ( 'failed' !== $status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the details of the failed refund so that we can find the associated order and write a note.
|
||||
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
|
||||
$refund_id = $this->read_webhook_property( $event_object, 'id' );
|
||||
$amount = $this->read_webhook_property( $event_object, 'amount' );
|
||||
$currency = $this->read_webhook_property( $event_object, 'currency' );
|
||||
|
||||
// Look up the order related to this charge.
|
||||
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
|
||||
if ( ! $order ) {
|
||||
throw new Invalid_Payment_Method_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
),
|
||||
'order_not_found'
|
||||
);
|
||||
}
|
||||
|
||||
$note = sprintf(
|
||||
WC_Payments_Utils::esc_interpolated_html(
|
||||
/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
|
||||
__( 'A refund of %1$s was <strong>unsuccessful</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
|
||||
[
|
||||
'strong' => '<strong>',
|
||||
'code' => '<code>',
|
||||
]
|
||||
),
|
||||
WC_Payments_Explicit_Price_Formatter::get_explicit_price(
|
||||
wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
|
||||
$order
|
||||
),
|
||||
'WooPayments',
|
||||
$refund_id
|
||||
);
|
||||
|
||||
if ( $this->order_service->order_note_exists( $order, $note ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refunds from order and delete refund if matches wcpay refund id.
|
||||
*
|
||||
* @var $wc_refunds WC_Order_Refund[]
|
||||
* */
|
||||
$wc_refunds = $order->get_refunds();
|
||||
if ( ! empty( $wc_refunds ) ) {
|
||||
foreach ( $wc_refunds as $wc_refund ) {
|
||||
$wcpay_refund_id = $this->order_service->get_wcpay_refund_id_for_order( $wc_refund );
|
||||
if ( $refund_id === $wcpay_refund_id ) {
|
||||
// Delete WC Refund.
|
||||
$wc_refund->delete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update order status if order is fully refunded.
|
||||
$current_order_status = $order->get_status();
|
||||
if ( Order_Status::REFUNDED === $current_order_status ) {
|
||||
$order->update_status( Order_Status::FAILED );
|
||||
}
|
||||
|
||||
$order->add_order_note( $note );
|
||||
$this->order_service->set_wcpay_refund_status_for_order( $order, 'failed' );
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook for an expired uncaptured payment.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
* @throws Invalid_Payment_Method_Exception When unable to resolve charge ID to order.
|
||||
*/
|
||||
private function process_webhook_expired_authorization( $event_body ) {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
|
||||
// Fetch the details of the expired auth so that we can find the associated order.
|
||||
$charge_id = $this->read_webhook_property( $event_object, 'id' );
|
||||
|
||||
// Look up the order related to this charge.
|
||||
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
|
||||
if ( ! $order ) {
|
||||
throw new Invalid_Payment_Method_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
),
|
||||
'order_not_found'
|
||||
);
|
||||
}
|
||||
|
||||
// Get the intent_id and then its status.
|
||||
$intent_id = $event_object['payment_intent'] ?? $order->get_meta( '_intent_id' );
|
||||
|
||||
$request = Get_Intention::create( $intent_id );
|
||||
$request->set_hook_args( $order );
|
||||
$intent = $request->send();
|
||||
|
||||
$intent_status = $intent->get_status();
|
||||
|
||||
// TODO: Revisit this logic once we support partial captures or multiple charges for order. We'll need to handle the "payment_intent.canceled" event too.
|
||||
$this->order_service->mark_payment_capture_expired( $order, $intent_id, $intent_status, $charge_id );
|
||||
|
||||
// Clear the authorization summary cache to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook for a payment intent canceled event.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function process_webhook_payment_intent_canceled( $event_body ) {
|
||||
// Clear the authorization summary cache to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook for a payment intent amount capturable updated event.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function process_webhook_payment_intent_amount_capturable_updated( $event_body ) {
|
||||
// Clear the authorization summary cache to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook for a failed payment intent.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
* @throws Invalid_Payment_Method_Exception When unable to resolve charge ID to order.
|
||||
*/
|
||||
private function process_webhook_payment_intent_failed( $event_body ) {
|
||||
// Check to make sure we should process this according to the payment method.
|
||||
$charge_id = $event_body['data']['object']['charges']['data'][0]['id'] ?? '';
|
||||
$last_payment_error = $event_body['data']['object']['last_payment_error'] ?? null;
|
||||
$payment_method = $last_payment_error['payment_method'] ?? null;
|
||||
$payment_method_type = $payment_method['type'] ?? null;
|
||||
|
||||
$actionable_methods = [
|
||||
Payment_Method::CARD,
|
||||
Payment_Method::CARD_PRESENT,
|
||||
Payment_Method::US_BANK_ACCOUNT,
|
||||
Payment_Method::BECS,
|
||||
];
|
||||
|
||||
if ( empty( $payment_method_type ) || ! in_array( $payment_method_type, $actionable_methods, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the order and make sure it is an order and the payment methods match.
|
||||
$order = $this->get_order_from_event_body( $event_body );
|
||||
$payment_method_id = $payment_method['id'] ?? null;
|
||||
|
||||
if ( ! $order || empty( $payment_method_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Payment_Method::CARD_PRESENT !== $payment_method_type && $payment_method_id !== $order->get_meta( '_payment_method_id' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
$intent_id = $this->read_webhook_property( $event_object, 'id' );
|
||||
$intent_status = $this->read_webhook_property( $event_object, 'status' );
|
||||
if ( Payment_Method::CARD_PRESENT === $payment_method_type ) {
|
||||
$this->order_service->mark_terminal_payment_failed( $order, $intent_id, $intent_status, $charge_id, $this->get_failure_message_from_error( $last_payment_error ) );
|
||||
} else {
|
||||
$this->order_service->mark_payment_failed( $order, $intent_id, $intent_status, $charge_id, $this->get_failure_message_from_error( $last_payment_error ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook for a successful payment intent.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
* @throws Invalid_Payment_Method_Exception When unable to resolve intent ID to order.
|
||||
*/
|
||||
private function process_webhook_payment_intent_succeeded( $event_body ) {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
$intent_id = $this->read_webhook_property( $event_object, 'id' );
|
||||
$currency = $this->read_webhook_property( $event_object, 'currency' );
|
||||
$order = $this->get_order_from_event_body( $event_body );
|
||||
$intent_status = $this->read_webhook_property( $event_object, 'status' );
|
||||
$event_charges = $this->read_webhook_property( $event_object, 'charges' );
|
||||
$charges_data = $this->read_webhook_property( $event_charges, 'data' );
|
||||
$charge_id = $this->read_webhook_property( $charges_data[0], 'id' );
|
||||
$metadata = $this->read_webhook_property( $event_object, 'metadata' );
|
||||
|
||||
$payment_method_id = $charges_data[0]['payment_method'] ?? null;
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update missing intents because webhook can be delivered before order is processed on the client.
|
||||
$meta_data_to_update = [
|
||||
'_intent_id' => $intent_id,
|
||||
'_charge_id' => $charge_id,
|
||||
'_payment_method_id' => $payment_method_id,
|
||||
WC_Payments_Utils::ORDER_INTENT_CURRENCY_META_KEY => $currency,
|
||||
];
|
||||
|
||||
// Save mandate id, necessary for some subscription renewals.
|
||||
$mandate_id = $event_data['object']['charges']['data'][0]['payment_method_details']['card']['mandate'] ?? null;
|
||||
if ( $mandate_id ) {
|
||||
$meta_data_to_update['_stripe_mandate_id'] = $mandate_id;
|
||||
}
|
||||
|
||||
$application_fee_amount = $charges_data[0]['application_fee_amount'] ?? null;
|
||||
if ( $application_fee_amount ) {
|
||||
$meta_data_to_update['_wcpay_transaction_fee'] = WC_Payments_Utils::interpret_stripe_amount( $application_fee_amount, $currency );
|
||||
}
|
||||
|
||||
foreach ( $meta_data_to_update as $key => $value ) {
|
||||
// Override existing meta data with incoming values, if present.
|
||||
if ( $value ) {
|
||||
$order->update_meta_data( $key, $value );
|
||||
}
|
||||
}
|
||||
// Save the order after updating the meta data values.
|
||||
$order->save();
|
||||
|
||||
// This is an incoming request from WCPay server rather than an outgoing request to WCPay server.
|
||||
// However, the shape of the payment intent object are the same.
|
||||
// Using this extraction method will reduce the code duplication.
|
||||
$payment_intent = $this->api_client->deserialize_payment_intention_object_from_array( $event_object );
|
||||
$this->order_service->update_order_status_from_intent( $order, $payment_intent );
|
||||
|
||||
$payment_method = $charges_data[0]['payment_method_details']['type'] ?? null;
|
||||
// Send the customer a card reader receipt if it's an in person payment type.
|
||||
if ( Payment_Method::CARD_PRESENT === $payment_method || Payment_Method::INTERAC_PRESENT === $payment_method ) {
|
||||
$merchant_settings = [
|
||||
'business_name' => $this->wcpay_gateway->get_option( 'account_business_name' ),
|
||||
'support_info' => [
|
||||
'address' => $this->wcpay_gateway->get_option( 'account_business_support_address' ),
|
||||
'phone' => $this->wcpay_gateway->get_option( 'account_business_support_phone' ),
|
||||
'email' => $this->wcpay_gateway->get_option( 'account_business_support_email' ),
|
||||
],
|
||||
];
|
||||
$this->receipt_service->send_customer_ipp_receipt_email( $order, $merchant_settings, $charges_data[0] );
|
||||
}
|
||||
|
||||
// Clear the authorization summary cache to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook dispute created.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
*/
|
||||
private function process_webhook_dispute_created( $event_body ) {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
|
||||
$reason = $this->read_webhook_property( $event_object, 'reason' );
|
||||
$amount_raw = $this->read_webhook_property( $event_object, 'amount' );
|
||||
$evidence = $this->read_webhook_property( $event_object, 'evidence_details' );
|
||||
$status = $this->read_webhook_property( $event_object, 'status' );
|
||||
$due_by = $this->read_webhook_property( $evidence, 'due_by' );
|
||||
|
||||
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
|
||||
|
||||
$currency = $order->get_currency();
|
||||
$amount_string = wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount_raw, $currency ), [ 'currency' => strtoupper( $currency ) ] );
|
||||
|
||||
// Explicitly add currency info if needed (multi-currency stores).
|
||||
$amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price_with_currency( $amount_string, $currency );
|
||||
|
||||
// Convert due_by to a date string in the store timezone.
|
||||
$due_by = date_i18n( wc_date_format(), $due_by );
|
||||
|
||||
if ( ! $order ) {
|
||||
throw new Invalid_Webhook_Data_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->order_service->mark_payment_dispute_created( $order, $charge_id, $amount, $reason, $due_by, $status );
|
||||
|
||||
// Clear dispute caches to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::ACTIVE_DISPUTES_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook dispute closed.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
*/
|
||||
private function process_webhook_dispute_closed( $event_body ) {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
|
||||
$status = $this->read_webhook_property( $event_object, 'status' );
|
||||
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
throw new Invalid_Webhook_Data_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->order_service->mark_payment_dispute_closed( $order, $charge_id, $status );
|
||||
|
||||
// Clear dispute caches to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::ACTIVE_DISPUTES_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook dispute updated.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
*/
|
||||
private function process_webhook_dispute_updated( $event_body ) {
|
||||
$event_type = $this->read_webhook_property( $event_body, 'type' );
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
$charge_id = $this->read_webhook_property( $event_object, 'charge' );
|
||||
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
throw new Invalid_Webhook_Data_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
switch ( $event_type ) {
|
||||
case 'charge.dispute.funds_withdrawn':
|
||||
$message = __( 'Payment dispute and fees have been deducted from your next payout', 'woocommerce-payments' );
|
||||
break;
|
||||
case 'charge.dispute.funds_reinstated':
|
||||
$message = __( 'Payment dispute funds have been reinstated', 'woocommerce-payments' );
|
||||
break;
|
||||
default:
|
||||
$message = __( 'Payment dispute has been updated', 'woocommerce-payments' );
|
||||
}
|
||||
|
||||
$note = sprintf(
|
||||
/* translators: %1: the dispute message, %2: the dispute details URL */
|
||||
__( '%1$s. See <a href="%2$s">dispute overview</a> for more details.', 'woocommerce-payments' ),
|
||||
$message,
|
||||
add_query_arg(
|
||||
[ 'id' => $charge_id ],
|
||||
admin_url( 'admin.php?page=wc-admin&path=/payments/transactions/details' )
|
||||
)
|
||||
);
|
||||
|
||||
if ( $this->order_service->order_note_exists( $order, $note ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->add_order_note( $note );
|
||||
|
||||
// Clear dispute caches to trigger a fetch of new data.
|
||||
$this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY );
|
||||
$this->database_cache->delete( DATABASE_CACHE::ACTIVE_DISPUTES_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process notification data.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception When data is not valid.
|
||||
*/
|
||||
private function process_wcpay_notification( array $event_body ) {
|
||||
$note = $this->read_webhook_property( $event_body, 'data' );
|
||||
|
||||
// Convert exception Rest_Request_Exception to Invalid_Webhook_Data_Exception
|
||||
// to be compatible with the expected exception in process().
|
||||
try {
|
||||
$this->remote_note_service->put_note( $note );
|
||||
} catch ( Rest_Request_Exception $e ) {
|
||||
throw new Invalid_Webhook_Data_Exception( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely get a value from the webhook event body array.
|
||||
*
|
||||
* @param array $array Array to read from.
|
||||
* @param string $key ID to fetch on.
|
||||
*
|
||||
* @return string|array|int|bool
|
||||
* @throws Invalid_Webhook_Data_Exception Thrown if ID not set.
|
||||
*/
|
||||
private function read_webhook_property( $array, $key ) {
|
||||
if ( ! isset( $array[ $key ] ) ) {
|
||||
throw new Invalid_Webhook_Data_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: ID being fetched */
|
||||
__( '%1$s not found in array', 'woocommerce-payments' ),
|
||||
$key
|
||||
)
|
||||
);
|
||||
}
|
||||
return $array[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely check whether a webhook contains a property.
|
||||
*
|
||||
* @param array $array Array to read from.
|
||||
* @param string $key ID to fetch on.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_webhook_property( $array, $key ) {
|
||||
return isset( $array[ $key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order related to the event.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
* @throws Invalid_Payment_Method_Exception When unable to resolve intent ID to order.
|
||||
*
|
||||
* @return null|WC_Order
|
||||
*/
|
||||
private function get_order_from_event_body( $event_body ) {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
$intent_id = $this->read_webhook_property( $event_object, 'id' );
|
||||
|
||||
// Look up the order related to this intent.
|
||||
$order = $this->wcpay_db->order_from_intent_id( $intent_id );
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
// Retrieving order with order_id in case intent_id was not properly set.
|
||||
Logger::debug( 'intent_id not found, using order_id to retrieve order' );
|
||||
$metadata = $this->read_webhook_property( $event_object, 'metadata' );
|
||||
$order_id = $metadata['order_id'] ?? null;
|
||||
// If metadata order id is null, try to read from the charges metadata.
|
||||
if ( null === $order_id ) {
|
||||
$charges = $this->read_webhook_property( $event_object, 'charges' );
|
||||
$charge = $charges[0] ?? [];
|
||||
$order_id = $charge['metadata']['order_id'] ?? null;
|
||||
}
|
||||
|
||||
if ( $order_id ) {
|
||||
$order = $this->wcpay_db->order_from_order_id( $order_id );
|
||||
} elseif ( ! empty( $event_object['invoice'] ) ) {
|
||||
// If the payment intent contains an invoice it is a WCPay Subscription-related intent and will be handled by the `invoice.paid` event.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
throw new Invalid_Payment_Method_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: intent ID */
|
||||
__( 'Could not find order via intent ID: %1$s', 'woocommerce-payments' ),
|
||||
$intent_id
|
||||
),
|
||||
'order_not_found'
|
||||
);
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the proper failure message from the code in the error.
|
||||
* Error codes from https://stripe.com/docs/error-codes.
|
||||
*
|
||||
* @param array $error The last payment error from the payment failed event.
|
||||
*
|
||||
* @return string The failure message.
|
||||
*/
|
||||
private function get_failure_message_from_error( $error ): string {
|
||||
$code = $error['code'] ?? '';
|
||||
$decline_code = $error['decline_code'] ?? '';
|
||||
$message = $error['message'] ?? '';
|
||||
|
||||
switch ( $code ) {
|
||||
case 'account_closed':
|
||||
return __( "The customer's bank account has been closed.", 'woocommerce-payments' );
|
||||
case 'debit_not_authorized':
|
||||
return __( 'The customer has notified their bank that this payment was unauthorized.', 'woocommerce-payments' );
|
||||
case 'insufficient_funds':
|
||||
return __( "The customer's account has insufficient funds to cover this payment.", 'woocommerce-payments' );
|
||||
case 'no_account':
|
||||
return __( "The customer's bank account could not be located.", 'woocommerce-payments' );
|
||||
case 'payment_method_microdeposit_failed':
|
||||
return __( 'Microdeposit transfers failed. Please check the account, institution and transit numbers.', 'woocommerce-payments' );
|
||||
case 'payment_method_microdeposit_verification_attempts_exceeded':
|
||||
return __( 'You have exceeded the number of allowed verification attempts.', 'woocommerce-payments' );
|
||||
case 'payment_intent_mandate_invalid':
|
||||
return __( 'The mandate used for this renewal payment is invalid. You may need to bring the customer back to your store and ask them to resubmit their payment information.', 'woocommerce-payments' );
|
||||
case 'card_declined':
|
||||
switch ( $decline_code ) {
|
||||
case 'debit_notification_undelivered':
|
||||
return __( "The customer's bank could not send pre-debit notification for the payment.", 'woocommerce-payments' );
|
||||
case 'transaction_not_approved':
|
||||
return __( 'For recurring payment greater than mandate amount or INR 15000, payment was not approved by the card holder.', 'woocommerce-payments' );
|
||||
}
|
||||
}
|
||||
|
||||
// translators: %s Stripe error message.
|
||||
return sprintf( __( 'With the following message: <code>%s</code>', 'woocommerce-payments' ), $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process webhook refund for events triggered externally.
|
||||
*
|
||||
* @param array $event_body The event that triggered the webhook.
|
||||
*
|
||||
* @throws Invalid_Webhook_Data_Exception Required parameters not found.
|
||||
* @throws Invalid_Webhook_Data_Exception When the refund amount is not valid.
|
||||
* @throws Order_Not_Found_Exception When unable to resolve charge ID to order.
|
||||
*/
|
||||
private function process_webhook_refund_triggered_externally( array $event_body ): void {
|
||||
$event_data = $this->read_webhook_property( $event_body, 'data' );
|
||||
$event_object = $this->read_webhook_property( $event_data, 'object' );
|
||||
|
||||
$is_refunded_event = isset( $event_body['type'] ) && 'charge.refunded' === $event_body['type'];
|
||||
$status = $this->read_webhook_property( $event_object, 'status' );
|
||||
if ( 'succeeded' !== $status || ! $is_refunded_event ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the details of the refund so that we can find the associated order and write a note.
|
||||
$charge_id = $this->read_webhook_property( $event_object, 'id' );
|
||||
$refund = $this->read_webhook_property( $event_object, 'refunds' )['data'][0]; // Most recent refund.
|
||||
$refund_id = $refund['id'] ?? '';
|
||||
$refund_reason = $refund['reason'] ?? '';
|
||||
$refund_balance_transaction_id = $refund['balance_transaction'] ?? '';
|
||||
$charge_amount = $this->read_webhook_property( $event_object, 'amount' );
|
||||
$currency = $this->read_webhook_property( $event_object, 'currency' );
|
||||
$refunded_amount = WC_Payments_Utils::interpret_stripe_amount( $refund['amount'], $currency );
|
||||
$is_partial_refund = $refund['amount'] < $charge_amount;
|
||||
|
||||
// Look up the order related to this charge.
|
||||
$order = $this->wcpay_db->order_from_charge_id( $charge_id );
|
||||
if ( ! $order ) {
|
||||
throw new Order_Not_Found_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
),
|
||||
'order_not_found'
|
||||
);
|
||||
}
|
||||
// Only care about refunds that are triggered externally, i.e. outside WP Admin.
|
||||
// Refunds triggered in WP Admin are handled by WC_Payment_Gateway_WCPay::process_refund.
|
||||
$wc_refunds = $order->get_refunds();
|
||||
if ( ! empty( $wc_refunds ) ) {
|
||||
foreach ( $wc_refunds as $wc_refund ) {
|
||||
$wcpay_refund_id = $this->order_service->get_wcpay_refund_id_for_order( $wc_refund );
|
||||
if ( $refund_id === $wcpay_refund_id ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $charge_amount < 0 || $refunded_amount > $order->get_total() ) {
|
||||
throw new Invalid_Webhook_Data_Exception(
|
||||
sprintf(
|
||||
/* translators: %1: charge ID */
|
||||
__( 'The refund amount is not valid for charge ID: %1$s', 'woocommerce-payments' ),
|
||||
$charge_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, ( ! $is_partial_refund ? $order->get_items() : [] ) );
|
||||
// Process the refund in the order service.
|
||||
$this->order_service->add_note_and_metadata_for_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id );
|
||||
}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* WC_Payments_Webhook_Reliability_Service class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use WCPay\Exceptions\API_Exception;
|
||||
use WCPay\Exceptions\Invalid_Webhook_Data_Exception;
|
||||
use WCPay\Logger;
|
||||
|
||||
/**
|
||||
* Improve webhook reliability by fetching failed events from the server,
|
||||
* then process them with ActionScheduler
|
||||
*/
|
||||
class WC_Payments_Webhook_Reliability_Service {
|
||||
const CONTINUOUS_FETCH_FLAG_EVENTS_LIST = 'has_more';
|
||||
const CONTINUOUS_FETCH_FLAG_ACCOUNT_DATA = 'has_more_failed_events';
|
||||
const WEBHOOK_FETCH_EVENTS_ACTION = 'wcpay_webhook_fetch_events';
|
||||
const WEBHOOK_PROCESS_EVENT_ACTION = 'wcpay_webhook_process_event';
|
||||
|
||||
/**
|
||||
* Client for making requests to the WooCommerce Payments API.
|
||||
*
|
||||
* @var WC_Payments_API_Client
|
||||
*/
|
||||
private $payments_api_client;
|
||||
|
||||
/**
|
||||
* WC_Payments_Action_Scheduler_Service.
|
||||
*
|
||||
* @var WC_Payments_Action_Scheduler_Service
|
||||
*/
|
||||
private $action_scheduler_service;
|
||||
|
||||
/**
|
||||
* Webhook Processing Service.
|
||||
*
|
||||
* @var WC_Payments_Webhook_Processing_Service
|
||||
*/
|
||||
private $webhook_processing_service;
|
||||
|
||||
/**
|
||||
* WC_Payments_Webhook_Reliability_Service constructor.
|
||||
*
|
||||
* @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client.
|
||||
* @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Wrapper for ActionScheduler service.
|
||||
* @param WC_Payments_Webhook_Processing_Service $webhook_processing_service WC_Payments_Webhook_Processing_Service instance.
|
||||
*/
|
||||
public function __construct(
|
||||
WC_Payments_API_Client $payments_api_client,
|
||||
WC_Payments_Action_Scheduler_Service $action_scheduler_service,
|
||||
WC_Payments_Webhook_Processing_Service $webhook_processing_service
|
||||
) {
|
||||
$this->payments_api_client = $payments_api_client;
|
||||
$this->action_scheduler_service = $action_scheduler_service;
|
||||
$this->webhook_processing_service = $webhook_processing_service;
|
||||
|
||||
// Note: Sometimes the `woocommerce_payments_account_refreshed` hook is ran before ActionScheduler is initialized.
|
||||
// In that case, we will not be able to schedule jobs. We will just ignore it.
|
||||
add_action( 'woocommerce_payments_account_refreshed', [ $this, 'maybe_schedule_fetch_events' ] );
|
||||
add_action( self::WEBHOOK_FETCH_EVENTS_ACTION, [ $this, 'fetch_events_and_schedule_processing_jobs' ] );
|
||||
add_action( self::WEBHOOK_PROCESS_EVENT_ACTION, [ $this, 'process_event' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* During the account data refresh, check the relevant flag to remaining failed events on the WooCommerce Payments server,
|
||||
* and decide whether scheduling a job to fetch them.
|
||||
*
|
||||
* @param mixed|array $account Account data retrieved from WooCommerce Payments server.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_schedule_fetch_events( $account ) {
|
||||
if ( ! is_array( $account ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $account[ self::CONTINUOUS_FETCH_FLAG_ACCOUNT_DATA ] ?? false ) {
|
||||
$this->schedule_fetch_events();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch failed events from the WooCommerce Payments server through ActionScheduler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function fetch_events_and_schedule_processing_jobs() {
|
||||
try {
|
||||
$payload = $this->payments_api_client->get_failed_webhook_events();
|
||||
} catch ( API_Exception $e ) {
|
||||
Logger::error( 'Can not fetch failed events from the server. Error:' . $e->getMessage() );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $payload[ self::CONTINUOUS_FETCH_FLAG_EVENTS_LIST ] ?? false ) {
|
||||
$this->schedule_fetch_events();
|
||||
}
|
||||
|
||||
// Save the data, and schedule a job for each event.
|
||||
$events = $payload['data'] ?? [];
|
||||
foreach ( $events as $event ) {
|
||||
if ( ! isset( $event['id'] ) ) {
|
||||
Logger::error( 'Event ID does not exist. Event data: ' . var_export( $event, true ) ); // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->set_event_data( $event );
|
||||
$this->schedule_process_event( $event['id'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an event through ActionScheduler.
|
||||
*
|
||||
* @param string $event_id Event ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process_event( string $event_id ) {
|
||||
Logger::info( 'Start processing event: ' . $event_id );
|
||||
|
||||
$event_data = $this->get_event_data( $event_id );
|
||||
|
||||
$this->delete_event_data( $event_id );
|
||||
|
||||
if ( null === $event_data ) {
|
||||
Logger::error( 'Stop processing as no data available for event: ' . $event_id );
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->webhook_processing_service->process( $event_data );
|
||||
Logger::info( 'Successfully processed event ' . $event_id );
|
||||
} catch ( Invalid_Webhook_Data_Exception $e ) {
|
||||
Logger::error( 'Failed processing event ' . $event_id . '. Reason: ' . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a job to process an event later.
|
||||
*
|
||||
* @param string $event_id Event ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_process_event( string $event_id ) {
|
||||
$this->action_scheduler_service->schedule_job( time(), self::WEBHOOK_PROCESS_EVENT_ACTION, [ 'event_id' => $event_id ] );
|
||||
Logger::info( 'Successfully scheduled a job to process event: ' . $event_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a job to fetch failed events.
|
||||
*
|
||||
* We will bail if this is called too early and ActionScheduler is not initialized.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_fetch_events() {
|
||||
$this->action_scheduler_service->schedule_job( time(), self::WEBHOOK_FETCH_EVENTS_ACTION );
|
||||
Logger::info( 'Successfully scheduled a job to fetch failed events from the server.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transient name to interact with the storage.
|
||||
*
|
||||
* @param string $event_id Event ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_transient_name_for_event_id( string $event_id ): string {
|
||||
// Use md5 to overcome the limit of transient name (172 characters) while Stripe event ID can be up to 255.
|
||||
return 'wcpay_failed_event_' . md5( $event_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the event data.
|
||||
*
|
||||
* @param array $event_data Event data.
|
||||
*
|
||||
* @return bool True if the value was set, false otherwise.
|
||||
*/
|
||||
public function set_event_data( array $event_data ) {
|
||||
if ( ! isset( $event_data['id'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return set_transient( $this->get_transient_name_for_event_id( $event_data['id'] ), $event_data, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the event data.
|
||||
*
|
||||
* @param string $event_id Event ID.
|
||||
*
|
||||
* @return bool True if the event data is deleted, false otherwise.
|
||||
*/
|
||||
public function delete_event_data( string $event_id ): bool {
|
||||
return delete_transient( $this->get_transient_name_for_event_id( $event_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the event data. Return null if the data does not exist.
|
||||
*
|
||||
* @param string $event_id Event ID.
|
||||
*
|
||||
* @return ?array
|
||||
*/
|
||||
public function get_event_data( string $event_id ) {
|
||||
$data = get_transient( $this->get_transient_name_for_event_id( $event_id ) );
|
||||
return false === $data ? null : $data;
|
||||
}
|
||||
}
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_WooPay_Button_Handler
|
||||
* Adds support for the WooPay express checkout button.
|
||||
*
|
||||
* Borrowed heavily from the WC_Payments_Payment_Request_Button_Handler class.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// TODO: Not sure which of these are needed yet.
|
||||
use WCPay\Exceptions\Invalid_Price_Exception;
|
||||
use WCPay\Logger;
|
||||
use WCPay\Payment_Information;
|
||||
use WCPay\WooPay\WooPay_Session;
|
||||
use WCPay\WooPay\WooPay_Utilities;
|
||||
|
||||
/**
|
||||
* WC_Payments_WooPay_Button_Handler class.
|
||||
*/
|
||||
class WC_Payments_WooPay_Button_Handler {
|
||||
const BUTTON_LOCATIONS = 'platform_checkout_button_locations';
|
||||
|
||||
/**
|
||||
* WC_Payments_Account instance to get information about the account
|
||||
*
|
||||
* @var WC_Payments_Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
/**
|
||||
* WC_Payment_Gateway_WCPay instance.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* WooPay_Utilities instance.
|
||||
*
|
||||
* @var WooPay_Utilities
|
||||
*/
|
||||
private $woopay_utilities;
|
||||
|
||||
/**
|
||||
* Express Checkout Helper instance.
|
||||
*
|
||||
* @var WC_Payments_Express_Checkout_Button_Helper
|
||||
*/
|
||||
private $express_checkout_helper;
|
||||
|
||||
/**
|
||||
* Initialize class actions.
|
||||
*
|
||||
* @param WC_Payments_Account $account Account information.
|
||||
* @param WC_Payment_Gateway_WCPay $gateway WCPay gateway.
|
||||
* @param WooPay_Utilities $woopay_utilities WCPay gateway.
|
||||
* @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper.
|
||||
*/
|
||||
public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WooPay_Utilities $woopay_utilities, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper ) {
|
||||
$this->account = $account;
|
||||
$this->gateway = $gateway;
|
||||
$this->woopay_utilities = $woopay_utilities;
|
||||
$this->express_checkout_helper = $express_checkout_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates eligibility for WooPay via feature flag.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_woopay_eligible;
|
||||
|
||||
/**
|
||||
* Indicates whether WooPay is enabled.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_woopay_enabled;
|
||||
|
||||
/**
|
||||
* Indicates whether WooPay express checkout is enabled.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_woopay_express_button_enabled;
|
||||
|
||||
/**
|
||||
* Indicates whether WooPay and WooPay express checkout are enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_woopay_enabled() {
|
||||
return $this->is_woopay_eligible && $this->is_woopay_enabled && $this->is_woopay_express_button_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Checks if WCPay is enabled.
|
||||
if ( ! $this->gateway->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Checks if WooPay is enabled.
|
||||
$this->is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag.
|
||||
$this->is_woopay_enabled = 'yes' === $this->gateway->get_option( 'platform_checkout', 'no' );
|
||||
$this->is_woopay_express_button_enabled = WC_Payments_Features::is_woopay_express_checkout_enabled();
|
||||
|
||||
if ( ! $this->is_woopay_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't load for change payment method page.
|
||||
if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
return;
|
||||
}
|
||||
|
||||
// Create WooPay button location option if it doesn't exist and enable all locations by default.
|
||||
if ( ! array_key_exists( self::BUTTON_LOCATIONS, get_option( 'woocommerce_woocommerce_payments_settings' ) ) ) {
|
||||
if ( isset( $this->gateway->get_form_fields()[ self::BUTTON_LOCATIONS ]['options'] ) ) {
|
||||
$all_locations = $this->gateway->get_form_fields()[ self::BUTTON_LOCATIONS ]['options'];
|
||||
|
||||
$this->gateway->update_option( self::BUTTON_LOCATIONS, array_keys( $all_locations ) );
|
||||
WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, array_keys( $all_locations ) );
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
|
||||
|
||||
add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_woopay_config' ] );
|
||||
|
||||
add_action( 'wp_ajax_woopay_express_checkout_button_show_error_notice', [ $this, 'show_error_notice' ] );
|
||||
add_action( 'wp_ajax_nopriv_woopay_express_checkout_button_show_error_notice', [ $this, 'show_error_notice' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the woopay button config to wcpay_config.
|
||||
*
|
||||
* @param array $config The existing config array.
|
||||
*
|
||||
* @return array The modified config array.
|
||||
*/
|
||||
public function add_woopay_config( $config ) {
|
||||
$user = wp_get_current_user();
|
||||
|
||||
$config['woopayButton'] = $this->get_button_settings();
|
||||
$config['woopayButtonNonce'] = wp_create_nonce( 'woopay_button_nonce' );
|
||||
$config['addToCartNonce'] = wp_create_nonce( 'wcpay-add-to-cart' );
|
||||
$config['shouldShowWooPayButton'] = $this->should_show_woopay_button();
|
||||
$config['woopaySessionEmail'] = WooPay_Session::get_user_email( $user );
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load public scripts and styles.
|
||||
*/
|
||||
public function scripts() {
|
||||
// Don't load scripts if we should not show the button.
|
||||
if ( ! $this->should_show_woopay_button() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY_EXPRESS_BUTTON', 'dist/woopay-express-button' );
|
||||
|
||||
$wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) );
|
||||
|
||||
wp_add_inline_script(
|
||||
'WCPAY_WOOPAY_EXPRESS_BUTTON',
|
||||
"
|
||||
var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) );
|
||||
",
|
||||
'before'
|
||||
);
|
||||
|
||||
wp_set_script_translations( 'WCPAY_WOOPAY_EXPRESS_BUTTON', 'woocommerce-payments' );
|
||||
|
||||
wp_enqueue_script( 'WCPAY_WOOPAY_EXPRESS_BUTTON' );
|
||||
|
||||
WC_Payments_Utils::enqueue_style(
|
||||
'WCPAY_WOOPAY',
|
||||
plugins_url( 'dist/woopay.css', WCPAY_PLUGIN_FILE ),
|
||||
[],
|
||||
WCPAY_VERSION_NUMBER,
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error notice HTML.
|
||||
*/
|
||||
public function show_error_notice() {
|
||||
$is_nonce_valid = check_ajax_referer( 'woopay_button_nonce', false, false );
|
||||
|
||||
if ( ! $is_nonce_valid ) {
|
||||
wp_send_json_error(
|
||||
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
$message = isset( $_POST['message'] ) ? sanitize_text_field( wp_unslash( $_POST['message'] ) ) : '';
|
||||
|
||||
// $message has already been translated.
|
||||
wc_add_notice( $message, 'error' );
|
||||
$notice = wc_print_notices( true );
|
||||
|
||||
wp_send_json_success(
|
||||
[
|
||||
'notice' => $notice,
|
||||
]
|
||||
);
|
||||
|
||||
wp_die();
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings for the `button` attribute - they depend on the "grouped settings" flag value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_button_settings() {
|
||||
$common_settings = $this->express_checkout_helper->get_common_button_settings();
|
||||
$woopay_button_settings = [
|
||||
'size' => $this->gateway->get_option( 'payment_request_button_size' ),
|
||||
'context' => $this->express_checkout_helper->get_button_context(),
|
||||
];
|
||||
|
||||
return array_merge( $common_settings, $woopay_button_settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether Payment Request Button should be available on this page.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_show_woopay_button() {
|
||||
// WCPay is not available.
|
||||
$gateways = WC()->payment_gateways->get_available_payment_gateways();
|
||||
if ( ! isset( $gateways['woocommerce_payments'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// WooPay is not enabled.
|
||||
if ( ! $this->is_woopay_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Page not supported.
|
||||
if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if WooPay is available in the user country.
|
||||
if ( ! $this->woopay_utilities->is_country_available( $this->gateway ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Product page, but not available in settings.
|
||||
if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checkout page, but not available in settings.
|
||||
if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cart page, but not available in settings.
|
||||
if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Product page, but has unsupported product type.
|
||||
if ( $this->express_checkout_helper->is_product() && ! $this->is_product_supported() ) {
|
||||
Logger::log( 'Product page has unsupported product type ( WooPay Express button disabled )' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cart has unsupported product type.
|
||||
if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) {
|
||||
Logger::log( 'Items in the cart have unsupported product type ( WooPay Express button disabled )' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
// On product page for a subscription product, but not logged in, making WooPay unavailable.
|
||||
if ( $this->express_checkout_helper->is_product() ) {
|
||||
$current_product = wc_get_product();
|
||||
|
||||
if ( $current_product && $this->express_checkout_helper->is_product_subscription( $current_product ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// On cart or checkout page with a subscription product in cart, but not logged in, making WooPay unavailable.
|
||||
if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && class_exists( 'WC_Subscriptions_Cart' ) && WC_Subscriptions_Cart::cart_contains_subscription() ) {
|
||||
// Check cart for subscription products.
|
||||
return false;
|
||||
}
|
||||
|
||||
// If guest checkout is not allowed, and customer is not logged in, disable the WooPay button.
|
||||
if ( ! $this->woopay_utilities->is_guest_checkout_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: We need to do some research here and see if there are any product types that we
|
||||
* absolutely cannot support with WooPay at this time. There are some examples in the
|
||||
* `WC_Payments_Payment_Request_Button_Handler->is_product_supported()` method.
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the payment request button.
|
||||
*/
|
||||
public function display_woopay_button_html() {
|
||||
if ( ! $this->should_show_woopay_button() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_button_settings();
|
||||
|
||||
?>
|
||||
<div id="wcpay-woopay-button" data-product_page=<?php echo esc_attr( $this->express_checkout_helper->is_product() ); ?>>
|
||||
<?php // The WooPay express checkout button React component will go here. This is rendered as disabled for now, until the page is initialized. ?>
|
||||
<button
|
||||
class="woopay-express-button"
|
||||
aria-label="<?php esc_attr_e( 'WooPay', 'woocommerce-payments' ); ?>"
|
||||
data-type="<?php echo esc_attr( $settings['type'] ); ?>"
|
||||
data-theme="<?php echo esc_attr( $settings['theme'] ); ?>"
|
||||
data-size="<?php echo esc_attr( $settings['size'] ); ?>"
|
||||
style="height: <?php echo esc_attr( $settings['height'] ); ?>px; border-radius: <?php echo esc_attr( $settings['radius'] ); ?>px"
|
||||
disabled
|
||||
></button>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the product page has a product compatible with the WooPay Express button.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_product_supported() {
|
||||
$product = $this->express_checkout_helper->get_product();
|
||||
$is_supported = true;
|
||||
|
||||
if ( ! is_object( $product ) ) {
|
||||
$is_supported = false;
|
||||
}
|
||||
|
||||
// External/affiliate products are not supported.
|
||||
if ( is_a( $product, 'WC_Product' ) && $product->is_type( 'external' ) ) {
|
||||
$is_supported = false;
|
||||
}
|
||||
|
||||
// Pre Orders products to be charged upon release are not supported.
|
||||
if (
|
||||
class_exists( 'WC_Pre_Orders_Product' ) &&
|
||||
method_exists( 'WC_Pre_Orders_Product', 'product_is_charged_upon_release' ) &&
|
||||
WC_Pre_Orders_Product::product_is_charged_upon_release( $product )
|
||||
) {
|
||||
$is_supported = false;
|
||||
}
|
||||
|
||||
// WC Bookings require confirmation products are not supported.
|
||||
if (
|
||||
is_a( $product, 'WC_Product_Booking' ) &&
|
||||
method_exists( $product, 'get_requires_confirmation' ) &&
|
||||
$product->get_requires_confirmation()
|
||||
) {
|
||||
$is_supported = false;
|
||||
}
|
||||
|
||||
return apply_filters( 'wcpay_woopay_button_is_product_supported', $is_supported, $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart to see if the WooPay Express button supports all of its items.
|
||||
*
|
||||
* @todo Abstract this. This is a copy of the same method in the `WC_Payments_Payment_Request_Button_Handler` class.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_allowed_items_in_cart() {
|
||||
$is_supported = true;
|
||||
|
||||
/**
|
||||
* Psalm throws an error here even though we check the class existence.
|
||||
*
|
||||
* @psalm-suppress UndefinedClass
|
||||
*/
|
||||
// We don't support pre-order products to be paid upon release.
|
||||
if ( class_exists( 'WC_Pre_Orders_Cart' ) && class_exists( 'WC_Pre_Orders_Product' ) ) {
|
||||
if (
|
||||
WC_Pre_Orders_Cart::cart_contains_pre_order() &&
|
||||
WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() )
|
||||
) {
|
||||
$is_supported = false;
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters( 'wcpay_platform_checkout_button_are_cart_items_supported', $is_supported );
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_WooPay_Direct_Checkout
|
||||
* Adds support for WooPay direct checkout feature.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\WooPay\WooPay_Utilities;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class WC_Payments_WooPay_Direct_Checkout.
|
||||
*/
|
||||
class WC_Payments_WooPay_Direct_Checkout {
|
||||
/**
|
||||
* WooPay_Utilities instance.
|
||||
*
|
||||
* @var WooPay_Utilities
|
||||
*/
|
||||
private $woopay_utilities;
|
||||
|
||||
/**
|
||||
* Initialize class actions.
|
||||
*
|
||||
* @param WooPay_Utilities $woopay_utilities WooPay utilities.
|
||||
*/
|
||||
public function __construct( WooPay_Utilities $woopay_utilities ) {
|
||||
$this->woopay_utilities = $woopay_utilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'wp_footer', [ $this, 'scripts' ] );
|
||||
add_filter( 'woocommerce_create_order', [ $this, 'maybe_use_store_api_draft_order_id' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* This filter is used to ensure the session's store_api_draft_order is used, if it exists.
|
||||
* This prevents a bug where the store_api_draft_order is not used and instead, a new
|
||||
* order_awaiting_payment is created during the checkout request. The bug being evident
|
||||
* if a product had one remaining stock and the store_api_draft_order was reserving it,
|
||||
* an order would fail to be placed since when order_awaiting_payment is created, it would
|
||||
* not be able to reserve the one stock.
|
||||
*
|
||||
* @param int $order_id The order ID being used.
|
||||
* @return int|mixed The new order ID to use.
|
||||
*/
|
||||
public function maybe_use_store_api_draft_order_id( $order_id ) {
|
||||
// Only apply this filter during the checkout request.
|
||||
$is_checkout = defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT;
|
||||
// Only apply this filter if the order ID is not already defined.
|
||||
$is_already_defined_order_id = ! empty( $order_id );
|
||||
// Only apply this filter if the session doesn't already have an order_awaiting_payment.
|
||||
$is_order_awaiting_payment = isset( WC()->session->order_awaiting_payment );
|
||||
// Only apply this filter if draft order ID exists.
|
||||
$has_draft_order = ! empty( WC()->session->get( 'store_api_draft_order' ) );
|
||||
if ( ! $is_checkout || $is_already_defined_order_id || $is_order_awaiting_payment || ! $has_draft_order ) {
|
||||
return $order_id;
|
||||
}
|
||||
|
||||
$draft_order_id = absint( WC()->session->get( 'store_api_draft_order' ) );
|
||||
// Set the order status to "pending" payment, so that it can be resumed.
|
||||
$draft_order = wc_get_order( $draft_order_id );
|
||||
$draft_order->set_status( 'pending' );
|
||||
$draft_order->save();
|
||||
|
||||
// Move $draft_order_id in session, from store_api_draft_order to order_awaiting_payment.
|
||||
WC()->session->set( 'store_api_draft_order', null );
|
||||
WC()->session->set( 'order_awaiting_payment', $draft_order_id );
|
||||
|
||||
return $order_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function scripts() {
|
||||
if ( ! $this->should_enqueue_scripts() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->woopay_utilities->should_enable_woopay_on_cart_or_checkout() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue the WCPay common config script only if it hasn't been enqueued yet.
|
||||
// This may happen when Direct Checkout is being enqueued on pages that are not the cart page,
|
||||
// such as the home and shop pages.
|
||||
if ( function_exists( 'did_filter' ) && did_filter( 'wcpay_payment_fields_js_config' ) === 0 ) {
|
||||
WC_Payments::enqueue_woopay_common_config_script();
|
||||
}
|
||||
|
||||
WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY_DIRECT_CHECKOUT', 'dist/woopay-direct-checkout' );
|
||||
|
||||
$direct_checkout_settings = [
|
||||
'params' => [
|
||||
'is_product_page' => $this->is_product_page(),
|
||||
],
|
||||
];
|
||||
wp_localize_script(
|
||||
'WCPAY_WOOPAY_DIRECT_CHECKOUT',
|
||||
'wcpayWooPayDirectCheckout',
|
||||
$direct_checkout_settings
|
||||
);
|
||||
|
||||
wp_enqueue_script( 'WCPAY_WOOPAY_DIRECT_CHECKOUT' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the direct checkout scripts should be enqueued on the page.
|
||||
*
|
||||
* Scripts should be enqueued if:
|
||||
* - The current page is the cart page.
|
||||
* - The current page has a cart block.
|
||||
* - The current page has the blocks mini cart widget, i.e 'woocommerce_blocks_cart_enqueue_data' has been fired.
|
||||
* - The current page has the cart fragments script enqueued. which is enqueued by the shortcode mini cart widget.
|
||||
*
|
||||
* @return bool True if the scripts should be enqueued, false otherwise.
|
||||
*/
|
||||
private function should_enqueue_scripts(): bool {
|
||||
return $this->is_cart_page()
|
||||
|| did_action( 'woocommerce_blocks_cart_enqueue_data' ) > 0
|
||||
|| ( wp_script_is( 'wc-cart-fragments', 'enqueued' ) && ! $this->is_checkout_page() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is the cart page.
|
||||
*
|
||||
* @return bool True if the current page is the cart page, false otherwise.
|
||||
*/
|
||||
private function is_cart_page(): bool {
|
||||
return is_cart() || has_block( 'woocommerce/cart' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is the checkout page.
|
||||
*
|
||||
* @return bool True if the current page is the checkout page, false otherwise.
|
||||
*/
|
||||
private function is_checkout_page(): bool {
|
||||
return is_checkout() || has_block( 'woocommerce/checkout' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is the product page.
|
||||
*
|
||||
* @return bool True if the current page is the product page, false otherwise.
|
||||
*/
|
||||
private function is_product_page() {
|
||||
return is_product() || wc_post_content_has_shortcode( 'product_page' );
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,645 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooPay_Tracker
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use Jetpack_Tracks_Client;
|
||||
use Jetpack_Tracks_Event;
|
||||
use WC_Payments;
|
||||
use WC_Payments_Features;
|
||||
use WCPay\Constants\Country_Code;
|
||||
use WP_Error;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* Track WooPay related events
|
||||
*/
|
||||
class WooPay_Tracker extends Jetpack_Tracks_Client {
|
||||
|
||||
/**
|
||||
* WCPay user event prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $user_prefix = 'wcpay';
|
||||
|
||||
/**
|
||||
* WooPay admin event prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $admin_prefix = 'wcadmin';
|
||||
|
||||
/**
|
||||
* WCPay http interface.
|
||||
*
|
||||
* @var Object
|
||||
*/
|
||||
private $http;
|
||||
|
||||
/**
|
||||
* Base URL for stats counter.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $pixel_base_url = 'https://pixel.wp.com/g.gif';
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WC_Payments_Http_Interface $http A class implementing WC_Payments_Http_Interface.
|
||||
*/
|
||||
public function __construct( $http ) {
|
||||
|
||||
$this->http = $http;
|
||||
|
||||
add_action( 'wp_ajax_platform_tracks', [ $this, 'ajax_tracks' ] );
|
||||
add_action( 'wp_ajax_nopriv_platform_tracks', [ $this, 'ajax_tracks' ] );
|
||||
add_action( 'wp_ajax_get_identity', [ $this, 'ajax_tracks_id' ] );
|
||||
add_action( 'wp_ajax_nopriv_get_identity', [ $this, 'ajax_tracks_id' ] );
|
||||
|
||||
// Actions that should result in recorded Tracks events.
|
||||
add_action( 'woocommerce_after_checkout_form', [ $this, 'classic_checkout_start' ] );
|
||||
add_action( 'woocommerce_after_cart', [ $this, 'classic_cart_page_view' ] );
|
||||
add_action( 'woocommerce_after_single_product', [ $this, 'classic_product_page_view' ] );
|
||||
add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', [ $this, 'blocks_checkout_start' ] );
|
||||
add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ $this, 'blocks_cart_page_view' ] );
|
||||
add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 );
|
||||
add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 );
|
||||
add_action( 'woocommerce_payments_save_user_in_woopay', [ $this, 'must_save_payment_method_to_platform' ] );
|
||||
add_action( 'wp_footer', [ $this, 'add_frontend_tracks_scripts' ] );
|
||||
add_action( 'before_woocommerce_pay_form', [ $this, 'pay_for_order_page_view' ] );
|
||||
add_action( 'woocommerce_thankyou', [ $this, 'thank_you_page_view' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Override jetpack-tracking's ajax handling to use internal maybe_record_event method.
|
||||
*/
|
||||
public function ajax_tracks() {
|
||||
// Check for nonce.
|
||||
if (
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
empty( $_REQUEST['tracksNonce'] ) || ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'platform_tracks_nonce' )
|
||||
) {
|
||||
wp_send_json_error(
|
||||
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! isset( $_REQUEST['tracksEventName'] ) ) {
|
||||
wp_send_json_error(
|
||||
__( 'No valid event name or type.', 'woocommerce-payments' ),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
$tracks_data = [];
|
||||
if ( isset( $_REQUEST['tracksEventProp'] ) ) {
|
||||
// tracksEventProp is a JSON-encoded string.
|
||||
$event_prop = json_decode( wc_clean( wp_unslash( $_REQUEST['tracksEventProp'] ) ), true );
|
||||
if ( is_array( $event_prop ) ) {
|
||||
$tracks_data = $event_prop;
|
||||
}
|
||||
}
|
||||
$this->maybe_record_event( sanitize_text_field( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data );
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks ID of the current user
|
||||
*/
|
||||
public function ajax_tracks_id() {
|
||||
$tracks_id = $this->tracks_get_identity();
|
||||
|
||||
if ( $tracks_id ) {
|
||||
wp_send_json_success( $tracks_id );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic method to track user events on WooPay enabled stores.
|
||||
*
|
||||
* @param string $event name of the event.
|
||||
* @param array $data array of event properties.
|
||||
*/
|
||||
public function maybe_record_event( $event, $data = [] ) {
|
||||
// Top level events should not be namespaced.
|
||||
if ( '_aliasUser' !== $event ) {
|
||||
$event = self::$user_prefix . '_' . $event;
|
||||
}
|
||||
|
||||
return $this->tracks_record_event( $event, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Track shopper events with the wcpay_prefix.
|
||||
*
|
||||
* @param string $event name of the event.
|
||||
* @param array $data array of event properties.
|
||||
* @param bool $record_on_frontend whether to record the event on the frontend to prevent cache break.
|
||||
*/
|
||||
public function maybe_record_wcpay_shopper_event( $event, $data = [], $record_on_frontend = true ) {
|
||||
$is_admin_event = false;
|
||||
$track_on_all_stores = true;
|
||||
|
||||
// Record the event immediately.
|
||||
if ( ! $record_on_frontend ) {
|
||||
// Top level events should not be namespaced.
|
||||
if ( '_aliasUser' !== $event ) {
|
||||
$event = self::$user_prefix . '_' . $event;
|
||||
}
|
||||
return $this->tracks_record_event( $event, $data, $is_admin_event, $track_on_all_stores );
|
||||
}
|
||||
|
||||
// Route the event through frontend to avoid setting cookies on page load.
|
||||
$data['record_event_data'] = compact( 'is_admin_event', 'track_on_all_stores' );
|
||||
|
||||
add_filter(
|
||||
'wcpay_frontend_tracks',
|
||||
function ( $tracks ) use ( $event, $data ) {
|
||||
$tracks[] = [
|
||||
'event' => $event,
|
||||
'properties' => $data,
|
||||
];
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to track admin events on all WCPay stores.
|
||||
*
|
||||
* @param string $event name of the event.
|
||||
* @param array $data array of event properties.
|
||||
*/
|
||||
public function maybe_record_admin_event( $event, $data = [] ) {
|
||||
// Top level events should not be namespaced.
|
||||
if ( '_aliasUser' !== $event ) {
|
||||
$event = self::$admin_prefix . '_' . $event;
|
||||
}
|
||||
|
||||
$is_admin_event = true;
|
||||
|
||||
return $this->tracks_record_event( $event, $data, $is_admin_event );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the store country is eligible for Tracks.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_country_tracks_eligible() {
|
||||
if ( ! function_exists( 'wc_get_base_location' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$store_base_location = wc_get_base_location();
|
||||
return ! empty( $store_base_location['country'] ) && Country_Code::UNITED_STATES === $store_base_location['country'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Override parent method to omit the jetpack TOS check and include custom tracking conditions.
|
||||
*
|
||||
* @param bool $is_admin_event Indicate whether the event is emitted from admin area.
|
||||
* @param bool $track_on_all_stores Indicate whether the event should be tracked on all stores.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_enable_tracking( $is_admin_event = false, $track_on_all_stores = false ) {
|
||||
|
||||
// Don't track if the gateway is not enabled.
|
||||
$gateway = \WC_Payments::get_gateway();
|
||||
if ( ! $gateway->is_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't track if the account is not connected.
|
||||
$account = WC_Payments::get_account_service();
|
||||
if ( is_null( $account ) || ! $account->is_stripe_connected() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't track any non-US stores.
|
||||
if ( ! $this->is_country_tracks_eligible() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always respect the user specific opt-out cookie.
|
||||
if ( ! empty( $_COOKIE['tk_opt-out'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Track all WooPay events from the admin area.
|
||||
if ( $is_admin_event ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For all other events ensure:
|
||||
// 1. Only site pages are tracked.
|
||||
// 2. Site Admin activity in site pages are not tracked.
|
||||
// 3. If track_on_all_stores is enabled, track all events regardless of WooPay eligibility.
|
||||
// 4. Otherwise, track only when WooPay is active.
|
||||
|
||||
// Track only site pages.
|
||||
if ( is_admin() && ! wp_doing_ajax() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't track site admins.
|
||||
if ( is_user_logged_in() && in_array( 'administrator', wp_get_current_user()->roles, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $track_on_all_stores ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For the remaining events, don't track when woopay is disabled.
|
||||
$is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag.
|
||||
$is_woopay_enabled = 'yes' === $gateway->get_option( 'platform_checkout', 'no' );
|
||||
if ( ! ( $is_woopay_eligible && $is_woopay_enabled ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an event in Tracks - this is the preferred way to record events from PHP.
|
||||
*
|
||||
* @param string $event_name The name of the event.
|
||||
* @param array $properties Custom properties to send with the event.
|
||||
* @param bool $is_admin_event Indicate whether the event is emitted from admin area.
|
||||
* @param bool $track_on_all_stores Indicate whether the event should be tracked on all stores.
|
||||
*
|
||||
* @return bool|array|\WP_Error|\Jetpack_Tracks_Event
|
||||
*/
|
||||
public function tracks_record_event( $event_name, $properties = [], $is_admin_event = false, $track_on_all_stores = false ) {
|
||||
|
||||
$user = wp_get_current_user();
|
||||
|
||||
// We don't want to track user events during unit tests/CI runs.
|
||||
if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$properties = apply_filters( 'wcpay_tracks_event_properties', $properties, $event_name );
|
||||
|
||||
if ( isset( $properties['record_event_data'] ) ) {
|
||||
if ( isset( $properties['record_event_data']['is_admin_event'] ) ) {
|
||||
$is_admin_event = $properties['record_event_data']['is_admin_event'];
|
||||
}
|
||||
|
||||
if ( isset( $properties['record_event_data']['track_on_all_stores'] ) ) {
|
||||
$track_on_all_stores = $properties['record_event_data']['track_on_all_stores'];
|
||||
}
|
||||
|
||||
unset( $properties['record_event_data'] );
|
||||
}
|
||||
|
||||
if ( ! $this->should_enable_tracking( $is_admin_event, $track_on_all_stores ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties );
|
||||
|
||||
if ( is_wp_error( $event_obj ) ) {
|
||||
return $event_obj;
|
||||
}
|
||||
|
||||
$pixel = $event_obj->build_pixel_url( $event_obj );
|
||||
|
||||
if ( ! $pixel ) {
|
||||
return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
|
||||
}
|
||||
|
||||
return self::record_pixel( $pixel );
|
||||
}
|
||||
|
||||
/**
|
||||
* Procedurally build a Tracks Event Object.
|
||||
*
|
||||
* @param \WP_User $user WP_user object.
|
||||
* @param string $event_name The name of the event.
|
||||
* @param array $properties Custom properties to send with the event.
|
||||
*
|
||||
* @return \Jetpack_Tracks_Event|\WP_Error
|
||||
*/
|
||||
private function tracks_build_event_obj( $user, $event_name, $properties = [] ) {
|
||||
$identity = $this->tracks_get_identity();
|
||||
$site_url = get_option( 'siteurl' );
|
||||
|
||||
$properties['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
|
||||
$properties['blog_url'] = $site_url;
|
||||
$properties['blog_id'] = \Jetpack_Options::get_option( 'id' );
|
||||
$properties['user_lang'] = $user->get( 'WPLANG' );
|
||||
|
||||
// Add event property for test mode vs. live mode events.
|
||||
$properties['test_mode'] = WC_Payments::mode()->is_test() ? 1 : 0;
|
||||
$properties['wcpay_version'] = WCPAY_VERSION_NUMBER;
|
||||
|
||||
// Add client's user agent to the event properties.
|
||||
if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
|
||||
$properties['_via_ua'] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) );
|
||||
}
|
||||
|
||||
$blog_details = [
|
||||
'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ),
|
||||
];
|
||||
|
||||
$timestamp = round( microtime( true ) * 1000 );
|
||||
$timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );
|
||||
|
||||
/**
|
||||
* Ignore incorrect argument definition in Jetpack_Tracks_Event.
|
||||
*
|
||||
* @psalm-suppress InvalidArgument
|
||||
*/
|
||||
return new \Jetpack_Tracks_Event(
|
||||
array_merge(
|
||||
$blog_details,
|
||||
(array) $properties,
|
||||
$identity,
|
||||
[
|
||||
'_en' => $event_name,
|
||||
'_ts' => $timestamp_string,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identity to send to tracks.
|
||||
*
|
||||
* @return array $identity
|
||||
*/
|
||||
public function tracks_get_identity() {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Meta is set, and user is still connected. Use WPCOM ID.
|
||||
$wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
|
||||
if ( $wpcom_id && $this->http->is_user_connected( $user_id ) ) {
|
||||
return [
|
||||
'_ut' => 'wpcom:user_id',
|
||||
'_ui' => $wpcom_id,
|
||||
];
|
||||
}
|
||||
|
||||
// User is connected, but no meta is set yet. Use WPCOM ID and set meta.
|
||||
if ( $this->http->is_user_connected( $user_id ) ) {
|
||||
$wpcom_user_data = $this->http->get_connected_user_data( $user_id );
|
||||
update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] );
|
||||
|
||||
return [
|
||||
'_ut' => 'wpcom:user_id',
|
||||
'_ui' => $wpcom_user_data['ID'],
|
||||
];
|
||||
}
|
||||
|
||||
// User isn't linked at all. Fall back to anonymous ID.
|
||||
$anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
|
||||
if ( ! $anon_id ) {
|
||||
$anon_id = \Jetpack_Tracks_Client::get_anon_id();
|
||||
add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
|
||||
}
|
||||
|
||||
return [
|
||||
'_ut' => 'anon',
|
||||
'_ui' => $anon_id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the classic checkout page has loaded.
|
||||
*/
|
||||
public function classic_checkout_start() {
|
||||
$is_woopay_enabled = WC_Payments_Features::is_woopay_enabled();
|
||||
$this->maybe_record_wcpay_shopper_event(
|
||||
'checkout_page_view',
|
||||
[
|
||||
'theme_type' => 'short_code',
|
||||
'woopay_enabled' => $is_woopay_enabled,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the blocks checkout page has loaded.
|
||||
*/
|
||||
public function blocks_checkout_start() {
|
||||
$is_woopay_enabled = WC_Payments_Features::is_woopay_enabled();
|
||||
$this->maybe_record_wcpay_shopper_event(
|
||||
'checkout_page_view',
|
||||
[
|
||||
'theme_type' => 'blocks',
|
||||
'woopay_enabled' => $is_woopay_enabled,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the classic cart page has loaded.
|
||||
*/
|
||||
public function classic_cart_page_view() {
|
||||
$this->maybe_record_wcpay_shopper_event(
|
||||
'cart_page_view',
|
||||
[
|
||||
'theme_type' => 'short_code',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the blocks cart page has loaded.
|
||||
*/
|
||||
public function blocks_cart_page_view() {
|
||||
$this->maybe_record_wcpay_shopper_event(
|
||||
'cart_page_view',
|
||||
[
|
||||
'theme_type' => 'blocks',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the classic cart product has loaded.
|
||||
*/
|
||||
public function classic_product_page_view() {
|
||||
$this->maybe_record_wcpay_shopper_event(
|
||||
'product_page_view',
|
||||
[
|
||||
'theme_type' => 'short_code',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the pay-for-order page has loaded.
|
||||
*/
|
||||
public function pay_for_order_page_view() {
|
||||
$this->maybe_record_wcpay_shopper_event(
|
||||
'pay_for_order_page_view'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bump a counter. No user identifiable information is sent.
|
||||
*
|
||||
* @param string $group The group to bump the stat in.
|
||||
* @param string $stat_name The name of the stat to bump.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function bump_stats( $group, $stat_name ) {
|
||||
$is_admin_event = false;
|
||||
$track_on_all_stores = true;
|
||||
|
||||
if ( ! $this->should_enable_tracking( $is_admin_event, $track_on_all_stores ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( WC_Payments::mode()->is_test() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pixel_url = sprintf(
|
||||
self::$pixel_base_url . '?v=wpcom-no-pv&x_%s=%s',
|
||||
$group,
|
||||
$stat_name
|
||||
);
|
||||
|
||||
$response = wp_remote_get( esc_url_raw( $pixel_url ) );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that the order has been processed.
|
||||
*
|
||||
* @param int $order_id The ID of the order.
|
||||
*/
|
||||
public function checkout_order_processed( $order_id ) {
|
||||
|
||||
$payment_gateway = wc_get_payment_gateway_by_order( $order_id );
|
||||
$properties = [ 'payment_title' => 'other' ];
|
||||
|
||||
// If the order was placed using WooCommerce Payments, record the payment title using Tracks.
|
||||
if ( isset( $payment_gateway->id ) && strpos( $payment_gateway->id, 'woocommerce_payments' ) === 0 ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
$payment_title = $order->get_payment_method_title();
|
||||
$properties = [ 'payment_title' => $payment_title ];
|
||||
|
||||
$is_woopay_order = ( isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT'] );
|
||||
|
||||
// Don't track WooPay orders. They will be tracked on WooPay side with more details.
|
||||
if ( ! $is_woopay_order ) {
|
||||
$this->maybe_record_wcpay_shopper_event( 'checkout_order_placed', $properties, false );
|
||||
}
|
||||
// If the order was placed using a different payment gateway, just increment a counter.
|
||||
} else {
|
||||
$this->bump_stats( 'wcpay_order_completed_gateway', 'other' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that user chose to save payment information in woopay.
|
||||
*/
|
||||
public function must_save_payment_method_to_platform() {
|
||||
$this->maybe_record_event(
|
||||
'woopay_registered',
|
||||
[
|
||||
'source' => 'checkout',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that Thank you page was viewed for a WCPay order.
|
||||
*
|
||||
* @param int $order_id The ID of the order.
|
||||
* @return void
|
||||
*/
|
||||
public function thank_you_page_view( $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order || 'woocommerce_payments' !== $order->get_payment_method() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->maybe_record_wcpay_shopper_event( 'order_success_page_view' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Tracks event that the WooPay express button locations has been updated.
|
||||
*
|
||||
* @param array $all_locations All pages where WooPay express button can be enabled.
|
||||
* @param array $platform_checkout_enabled_locations pages where WooPay express button is enabled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function woopay_locations_updated( $all_locations, $platform_checkout_enabled_locations ) {
|
||||
$props = [];
|
||||
foreach ( array_keys( $all_locations ) as $location ) {
|
||||
$key = $location . '_enabled';
|
||||
if ( in_array( $location, $platform_checkout_enabled_locations, true ) ) {
|
||||
$props[ $key ] = true;
|
||||
} else {
|
||||
$props[ $key ] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->maybe_record_admin_event( 'woopay_express_button_locations_updated', $props );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add front-end tracks scripts to prevent cache break.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_frontend_tracks_scripts() {
|
||||
$frontent_tracks = apply_filters( 'wcpay_frontend_tracks', [] );
|
||||
|
||||
if ( count( $frontent_tracks ) === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC_Payments::register_script_with_dependencies( 'wcpay-frontend-tracks', 'dist/frontend-tracks' );
|
||||
|
||||
// Define wcpayConfig before the frontend tracks script if it hasn't been defined yet.
|
||||
$wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) );
|
||||
wp_add_inline_script(
|
||||
'wcpay-frontend-tracks',
|
||||
"
|
||||
var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) );
|
||||
",
|
||||
'before'
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'wcpay-frontend-tracks',
|
||||
'wcPayFrontendTracks',
|
||||
$frontent_tracks
|
||||
);
|
||||
|
||||
wp_enqueue_script( 'wcpay-frontend-tracks' );
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Blocks_Data_Extractor
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\StoreApi;
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
|
||||
use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema;
|
||||
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
|
||||
|
||||
defined( 'ABSPATH' ) || exit; // block direct access.
|
||||
|
||||
/**
|
||||
* Extract data fields from certain block based plugins.
|
||||
*/
|
||||
class Blocks_Data_Extractor {
|
||||
|
||||
/**
|
||||
* Instance of the integration registry.
|
||||
*
|
||||
* @var IntegrationRegistry
|
||||
*/
|
||||
private $integration_registry;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->integration_registry = new IntegrationRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of available Blocks.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_available_blocks() {
|
||||
$blocks = [];
|
||||
if ( class_exists( '\AutomateWoo\Blocks\Marketing_Optin_Block' ) ) {
|
||||
// phpcs:ignore
|
||||
/**
|
||||
* @psalm-suppress UndefinedClass
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$blocks[] = new \Automatewoo\Blocks\Marketing_Optin_Block();
|
||||
}
|
||||
|
||||
if ( class_exists( '\Mailchimp_Woocommerce_Newsletter_Blocks_Integration' ) ) {
|
||||
// phpcs:ignore
|
||||
/**
|
||||
* @psalm-suppress UndefinedClass
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$blocks[] = new \Mailchimp_Woocommerce_Newsletter_Blocks_Integration();
|
||||
}
|
||||
|
||||
if ( class_exists( '\WCK\Blocks\CheckoutIntegration' ) ) {
|
||||
// phpcs:ignore
|
||||
/**
|
||||
* @psalm-suppress UndefinedClass
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$blocks[] = new \WCK\Blocks\CheckoutIntegration();
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all the blocks.
|
||||
*
|
||||
* @param array $blocks A list of blocks to register.
|
||||
* @return void
|
||||
*/
|
||||
private function register_blocks( $blocks ) {
|
||||
foreach ( $blocks as $block ) {
|
||||
$this->integration_registry->register( $block );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all blocks.
|
||||
*
|
||||
* @param array $blocks A list of blocks to unregister.
|
||||
* @return void
|
||||
*/
|
||||
private function unregister_blocks( $blocks ) {
|
||||
foreach ( $blocks as $block ) {
|
||||
$this->integration_registry->unregister( $block );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mailpoet's block registration is different from the other two plugins. Data fields are passed
|
||||
* from the parent class. This method fetches the data fields without registering the plugin.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_mailpoet_data() {
|
||||
// phpcs:ignore
|
||||
/**
|
||||
* We check whether relevant MailPoet classes exists before invoking this method.
|
||||
*
|
||||
* @psalm-suppress UndefinedClass
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$mailpoet_wc_subscription = \MailPoet\DI\ContainerWrapper::getInstance()->get( \MailPoet\WooCommerce\Subscription::class );
|
||||
// phpcs:ignore
|
||||
/**
|
||||
* @psalm-suppress UndefinedClass
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$settings_instance = \MailPoet\Settings\SettingsController::getInstance();
|
||||
$settings = [
|
||||
'defaultText' => $settings_instance->get( 'woocommerce.optin_on_checkout.message', '' ),
|
||||
'optinEnabled' => $settings_instance->get( 'woocommerce.optin_on_checkout.enabled', false ),
|
||||
'defaultStatus' => false,
|
||||
];
|
||||
|
||||
if ( version_compare( \MAILPOET_VERSION, '4.18.0', '<=' ) ) {
|
||||
$settings['defaultStatus'] = $mailpoet_wc_subscription->isCurrentUserSubscribed();
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_data() {
|
||||
$blocks = $this->get_available_blocks();
|
||||
|
||||
$this->register_blocks( $blocks );
|
||||
|
||||
$blocks_data = $this->integration_registry->get_all_registered_script_data();
|
||||
|
||||
if ( class_exists( 'MailPoet\DI\ContainerWrapper' ) && class_exists( 'MailPoet\WooCommerce\Subscription' ) ) {
|
||||
$blocks_data += [ 'mailpoet_data' => $this->get_mailpoet_data() ];
|
||||
}
|
||||
|
||||
$this->unregister_blocks( $blocks );
|
||||
|
||||
return $blocks_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the namespaces in the Store API checkout schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_checkout_schema_namespaces(): array {
|
||||
$namespaces = [];
|
||||
|
||||
if (
|
||||
class_exists( 'Automattic\WooCommerce\StoreApi\StoreApi' ) &&
|
||||
class_exists( 'Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema' ) &&
|
||||
class_exists( 'Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema' )
|
||||
) {
|
||||
try {
|
||||
$checkout_schema = StoreApi::container()->get( ExtendSchema::class )->get_endpoint_schema( CheckoutSchema::IDENTIFIER );
|
||||
} catch ( \Exception $e ) {
|
||||
return $namespaces;
|
||||
}
|
||||
|
||||
$namespaces = array_keys( (array) $checkout_schema );
|
||||
}
|
||||
|
||||
return $namespaces;
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Currency_Manager
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay;
|
||||
|
||||
use WC_Payment_Gateway_WCPay;
|
||||
use WCPay\Constants\Payment_Method;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* It ensures that when a payment method is added and multi-currency is enabled, the needed currency is also added.
|
||||
*/
|
||||
class WC_Payments_Currency_Manager {
|
||||
/**
|
||||
* The WCPay gateway class instance.
|
||||
*
|
||||
* @var WC_Payment_Gateway_WCPay
|
||||
*/
|
||||
private $gateway;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param WC_Payment_Gateway_WCPay $gateway The WCPay gateway class instance.
|
||||
*/
|
||||
public function __construct( WC_Payment_Gateway_WCPay $gateway ) {
|
||||
$this->gateway = $gateway;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'update_option_woocommerce_woocommerce_payments_settings', [ $this, 'maybe_add_missing_currencies' ] );
|
||||
add_action( 'admin_head', [ $this, 'add_payment_method_currency_dependencies_script' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the multi-currency instance or returns null if it's not available.
|
||||
* This method allows for easier testing by allowing the multi-currency instance to be mocked.
|
||||
*
|
||||
* @return \WCPay\MultiCurrency\MultiCurrency|null
|
||||
*/
|
||||
public function get_multi_currency_instance() {
|
||||
if ( ! function_exists( 'WC_Payments_Multi_Currency' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! WC_Payments_Multi_Currency()->is_initialized() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return WC_Payments_Multi_Currency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currencies needed per enabled payment method
|
||||
*
|
||||
* @return array The currencies keyed with the related payment method
|
||||
*/
|
||||
public function get_enabled_payment_method_currencies() {
|
||||
$enabled_payment_method_ids = $this->gateway->get_upe_enabled_payment_method_ids();
|
||||
$account_currency = $this->gateway->get_account_domestic_currency();
|
||||
$payment_methods_needing_currency = array_reduce(
|
||||
$enabled_payment_method_ids,
|
||||
function ( $result, $method ) use ( $account_currency ) {
|
||||
if ( in_array( $method, [ 'card', 'card_present' ], true ) ) {
|
||||
return $result;
|
||||
}
|
||||
try {
|
||||
$method_key = Payment_Method::search( $method );
|
||||
} catch ( \InvalidArgumentException $e ) {
|
||||
return $result;
|
||||
}
|
||||
$class_key = ucfirst( strtolower( $method_key ? $method_key : $method ) );
|
||||
$class_name = "\\WCPay\\Payment_Methods\\{$class_key}_Payment_Method";
|
||||
if ( ! class_exists( $class_name ) ) {
|
||||
return $result;
|
||||
}
|
||||
$payment_method_instance = new $class_name( null );
|
||||
|
||||
$result[ $method ] = [
|
||||
'currencies' => $payment_method_instance->has_domestic_transactions_restrictions() ? [ $account_currency ] : $payment_method_instance->get_currencies(),
|
||||
'title' => $payment_method_instance->get_title( $this->gateway->get_account_country() ),
|
||||
];
|
||||
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return $payment_methods_needing_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that when a payment method is added from the settings, the needed currency is also added.
|
||||
*/
|
||||
public function maybe_add_missing_currencies() {
|
||||
$multi_currency = $this->get_multi_currency_instance();
|
||||
if ( is_null( $multi_currency ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_methods_needing_currency = $this->get_enabled_payment_method_currencies();
|
||||
if ( empty( $payment_methods_needing_currency ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enabled_currencies = $multi_currency->get_enabled_currencies();
|
||||
$available_currencies = $multi_currency->get_available_currencies();
|
||||
|
||||
$missing_currency_codes = [];
|
||||
|
||||
// TODO: we need to find something about having a currency not available for the method in case of having disabled currencies in the future.
|
||||
// First option, not do display it if the available currency is blocked by something else (Stripe, merchant, WCPay etc.)
|
||||
// Second option, showing a notice that it can't be selected because the currency is not available to use.
|
||||
|
||||
// we have payments needing some currency being enabled, let's ensure the currency is present.
|
||||
foreach ( $payment_methods_needing_currency as $payment_method_data ) {
|
||||
$needed_currency_codes = $payment_method_data['currencies'];
|
||||
foreach ( $needed_currency_codes as $needed_currency_code ) {
|
||||
if ( ! isset( $available_currencies[ $needed_currency_code ] ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( isset( $enabled_currencies[ $needed_currency_code ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing_currency_codes[] = $needed_currency_code;
|
||||
}
|
||||
}
|
||||
|
||||
$missing_currency_codes = array_unique( $missing_currency_codes );
|
||||
|
||||
if ( empty( $missing_currency_codes ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The set_enabled_currencies method throws an exception if any currencies passed are not found in the current available currencies.
|
||||
* Any currencies not found are filtered out above, so we shouldn't need a try/catch here.
|
||||
*/
|
||||
$multi_currency->set_enabled_currencies( array_merge( array_keys( $enabled_currencies ), $missing_currency_codes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `multiCurrencyPaymentMethodsMap` JS object to the multi-currency settings page.
|
||||
*
|
||||
* This object maps currencies to payment methods that require them, so the multi-currency settings page displays a notice in case of dependencies.
|
||||
*/
|
||||
public function add_payment_method_currency_dependencies_script() {
|
||||
$multi_currency = $this->get_multi_currency_instance();
|
||||
|
||||
if ( is_null( $multi_currency ) || ! $multi_currency->is_multi_currency_settings_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_methods_needing_currency = $this->get_enabled_payment_method_currencies();
|
||||
if ( empty( $payment_methods_needing_currency ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currency_methods_map = [];
|
||||
foreach ( $payment_methods_needing_currency as $method => $data ) {
|
||||
foreach ( $data['currencies'] as $currency ) {
|
||||
if ( ! isset( $currency_methods_map[ $currency ] ) ) {
|
||||
$currency_methods_map[ $currency ] = [];
|
||||
}
|
||||
$currency_methods_map[ $currency ][ $method ] = $data['title'];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<script type='text/javascript'>
|
||||
window.multiCurrencyPaymentMethodsMap = <?php echo wp_json_encode( $currency_methods_map ); ?>;
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Main functions to start MultiCurrency class.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Load customer multi-currency if feature is enabled or if it is the setup page.
|
||||
*/
|
||||
function wcpay_multi_currency_onboarding_check() {
|
||||
$is_setup_page = false;
|
||||
|
||||
// Skip checking the HTTP referer if it is a cron job.
|
||||
if ( ! defined( 'DOING_CRON' ) ) {
|
||||
$http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) );
|
||||
if ( ! empty( $http_referer ) ) {
|
||||
$is_setup_page = strpos( $http_referer, 'multi-currency-setup' ) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
return $is_setup_page;
|
||||
}
|
||||
|
||||
if ( ! WC_Payments_Features::is_customer_multi_currency_enabled() && ! wcpay_multi_currency_onboarding_check() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MultiCurrency singleton.
|
||||
*
|
||||
* @return WCPay\MultiCurrency\MultiCurrency
|
||||
*/
|
||||
function WC_Payments_Multi_Currency() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
|
||||
static $instance = null;
|
||||
|
||||
if ( is_null( $instance ) ) {
|
||||
$instance = new WCPay\MultiCurrency\MultiCurrency(
|
||||
WC_Payments::get_settings_service(),
|
||||
WC_Payments::get_payments_api_client(),
|
||||
WC_Payments::get_account_service(),
|
||||
WC_Payments::get_localization_service(),
|
||||
WC_Payments::get_database_cache()
|
||||
);
|
||||
$instance->init_hooks();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
add_action( 'plugins_loaded', 'WC_Payments_Multi_Currency', 12 );
|
||||
|
||||
register_deactivation_hook( WCPAY_PLUGIN_FILE, 'wcpay_multi_currency_deactivated' );
|
||||
|
||||
/**
|
||||
* Plugin deactivation hook.
|
||||
*/
|
||||
function wcpay_multi_currency_deactivated() {
|
||||
WCPay\MultiCurrency\MultiCurrency::remove_woo_admin_notes();
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wc_get_currency_switcher_markup' ) ) {
|
||||
/**
|
||||
* Gets the switcher widget markup.
|
||||
*
|
||||
* @param array $instance The widget's instance settings.
|
||||
* @param array $args The widget's arguments.
|
||||
*
|
||||
* @return string The widget markup.
|
||||
*/
|
||||
function wc_get_currency_switcher_markup( array $instance = [], array $args = [] ): string {
|
||||
return WC_Payments_Multi_Currency()->get_switcher_widget_markup( $instance, $args );
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin email about payment retry failed due to authentication
|
||||
*
|
||||
* Email sent to admins when an attempt to automatically process a subscription renewal payment has failed
|
||||
* with the `authentication_needed` error, and a retry rule has been applied to retry the payment in the future.
|
||||
*
|
||||
* @extends WC_Email_Failed_Order
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
use WCPay\Logger;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* An email sent to the admin when payment fails to go through due to authentication_required error.
|
||||
*/
|
||||
class WC_Payments_Email_Failed_Authentication_Retry extends WC_Email_Failed_Order {
|
||||
|
||||
/**
|
||||
* The details of the last retry (if any) recorded for a given order
|
||||
*
|
||||
* @var WCS_Retry
|
||||
*/
|
||||
private $retry;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->id = 'failed_authentication_requested';
|
||||
$this->title = __( 'Payment authentication requested email', 'woocommerce-payments' );
|
||||
$this->description = __( 'Payment authentication requested emails are sent to chosen recipient(s) when an attempt to automatically process a subscription renewal payment fails because the transaction requires an SCA verification, the customer is requested to authenticate the payment, and a retry rule has been applied to notify the customer again within a certain time period.', 'woocommerce-payments' );
|
||||
|
||||
$this->heading = __( 'Automatic renewal payment failed due to authentication required', 'woocommerce-payments' );
|
||||
$this->subject = __( '[{site_title}] Automatic payment failed for {order_number}. Customer asked to authenticate payment and will be notified again {retry_time}', 'woocommerce-payments' );
|
||||
|
||||
$this->template_html = 'failed-renewal-authentication-requested.php';
|
||||
$this->template_plain = 'plain/failed-renewal-authentication-requested.php';
|
||||
$this->template_base = __DIR__ . '/emails/';
|
||||
|
||||
$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
|
||||
|
||||
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
|
||||
WC_Email::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default e-mail subject.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_subject() {
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default e-mail heading.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_heading() {
|
||||
return $this->heading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger.
|
||||
*
|
||||
* @param int $order_id The order ID.
|
||||
* @param WC_Order|null $order Order object.
|
||||
*/
|
||||
public function trigger( $order_id, $order = null ) {
|
||||
$this->object = $order;
|
||||
|
||||
$this->find['retry-time'] = '{retry_time}';
|
||||
if ( class_exists( 'WCS_Retry_Manager' ) && function_exists( 'wcs_get_human_time_diff' ) ) {
|
||||
$this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) );
|
||||
$this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() );
|
||||
} else {
|
||||
Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admin email about customer notification for authentication required for renewal payment.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->find['order-number'] = '{order_number}';
|
||||
$this->replace['order-number'] = $this->object->get_order_number();
|
||||
|
||||
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content html.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_html() {
|
||||
return wc_get_template_html(
|
||||
$this->template_html,
|
||||
[
|
||||
'order' => $this->object,
|
||||
'retry' => $this->retry,
|
||||
'email_heading' => $this->get_heading(),
|
||||
'sent_to_admin' => true,
|
||||
'plain_text' => false,
|
||||
'email' => $this,
|
||||
],
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content plain.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_plain() {
|
||||
return wc_get_template_html(
|
||||
$this->template_plain,
|
||||
[
|
||||
'order' => $this->object,
|
||||
'retry' => $this->retry,
|
||||
'email_heading' => $this->get_heading(),
|
||||
'sent_to_admin' => true,
|
||||
'plain_text' => true,
|
||||
'email' => $this,
|
||||
],
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
}
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Payments_Email_Failed_Renewal_Authentication
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed Renewal Authentication Notification.
|
||||
*
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WC_Payments_Email_Failed_Renewal_Authentication extends WC_Email {
|
||||
/**
|
||||
* An instance of the email, which would normally be sent after a failed payment.
|
||||
*
|
||||
* @var WC_Email_Failed_Order
|
||||
*/
|
||||
public $original_email;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WC_Email_Failed_Order[] $email_classes All existing instances of WooCommerce emails.
|
||||
*/
|
||||
public function __construct( $email_classes = [] ) {
|
||||
$this->id = 'failed_renewal_authentication';
|
||||
$this->title = __( 'Failed subscription renewal SCA authentication', 'woocommerce-payments' );
|
||||
$this->description = __( 'Sent to a customer when a renewal fails because the transaction requires an SCA verification. The email contains renewal order information and payment links.', 'woocommerce-payments' );
|
||||
$this->customer_email = true;
|
||||
|
||||
$this->template_html = 'failed-renewal-authentication.php';
|
||||
$this->template_plain = 'plain/failed-renewal-authentication.php';
|
||||
$this->template_base = __DIR__ . '/emails/';
|
||||
|
||||
// Triggers the email at the correct hook.
|
||||
add_action( 'woocommerce_woocommerce_payments_payment_requires_action', [ $this, 'trigger' ] );
|
||||
|
||||
if ( isset( $email_classes['WCS_Email_Customer_Renewal_Invoice'] ) ) {
|
||||
$this->original_email = $email_classes['WCS_Email_Customer_Renewal_Invoice'];
|
||||
}
|
||||
|
||||
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor.
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML for the email while keeping the `template_base` in mind.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_html() {
|
||||
ob_start();
|
||||
wc_get_template(
|
||||
$this->template_html,
|
||||
[
|
||||
'order' => $this->object,
|
||||
'email_heading' => $this->get_heading(),
|
||||
'sent_to_admin' => false,
|
||||
'plain_text' => false,
|
||||
'authorization_url' => $this->get_authorization_url( $this->object ),
|
||||
'email' => $this,
|
||||
],
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the plain text for the email while keeping the `template_base` in mind.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content_plain() {
|
||||
ob_start();
|
||||
wc_get_template(
|
||||
$this->template_plain,
|
||||
[
|
||||
'order' => $this->object,
|
||||
'email_heading' => $this->get_heading(),
|
||||
'sent_to_admin' => false,
|
||||
'plain_text' => true,
|
||||
'authorization_url' => $this->get_authorization_url( $this->object ),
|
||||
'email' => $this,
|
||||
],
|
||||
'',
|
||||
$this->template_base
|
||||
);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URL, which will be used to authenticate the payment.
|
||||
*
|
||||
* @param WC_Order $order The order whose payment needs authentication.
|
||||
* @return string
|
||||
*/
|
||||
public function get_authorization_url( $order ) {
|
||||
return add_query_arg( 'wcpay-confirmation', 1, $order->get_checkout_payment_url( false ) ); // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in.
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses specific fields from `WC_Email_Customer_Invoice` for this email.
|
||||
*/
|
||||
public function init_form_fields() {
|
||||
parent::init_form_fields();
|
||||
$base_fields = $this->form_fields;
|
||||
|
||||
$this->form_fields = [
|
||||
'enabled' => [
|
||||
'title' => _x( 'Enable/disable', 'an email notification', 'woocommerce-payments' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable this email notification', 'woocommerce-payments' ),
|
||||
'default' => 'yes',
|
||||
],
|
||||
|
||||
'subject' => $base_fields['subject'],
|
||||
'heading' => $base_fields['heading'],
|
||||
'email_type' => $base_fields['email_type'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default subject of the email (modifiable in settings).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_subject() {
|
||||
return __( 'Payment authorization needed for renewal of {site_title} order {order_number}', 'woocommerce-payments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default heading of the email (modifiable in settings).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_heading() {
|
||||
return __( 'Payment authorization needed for renewal of order {order_number}', 'woocommerce-payments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the email while also disconnecting the original Subscriptions email.
|
||||
*
|
||||
* @param WC_Order $order The order that is being paid.
|
||||
*/
|
||||
public function trigger( $order ) {
|
||||
if ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order->get_id() ) || wcs_is_subscription( $order->get_id() ) || wcs_order_contains_renewal( $order->get_id() ) ) ) {
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->object = $order;
|
||||
|
||||
$this->recipient = $order->get_billing_email();
|
||||
|
||||
$this->find['order_date'] = '{order_date}';
|
||||
$this->replace['order_date'] = wc_format_datetime( $order->get_date_created() );
|
||||
|
||||
$this->find['order_number'] = '{order_number}';
|
||||
$this->replace['order_number'] = $order->get_order_number();
|
||||
|
||||
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
|
||||
|
||||
// Prevent the renewal email from WooCommerce Subscriptions from being sent.
|
||||
if ( isset( $this->original_email ) ) {
|
||||
remove_action(
|
||||
'woocommerce_generated_manual_renewal_order_renewal_notification',
|
||||
[
|
||||
$this->original_email,
|
||||
'trigger',
|
||||
]
|
||||
);
|
||||
remove_action(
|
||||
'woocommerce_order_status_failed_renewal_notification',
|
||||
[
|
||||
$this->original_email,
|
||||
'trigger',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent the retry email from WooCommerce Subscriptions from being sent.
|
||||
add_filter( 'wcs_get_retry_rule_raw', [ $this, 'prevent_retry_notification_email' ], 100, 3 );
|
||||
|
||||
// Send email to store owner indicating communication is happening with the customer to request authentication.
|
||||
add_filter( 'wcs_get_retry_rule_raw', [ $this, 'set_store_owner_custom_email' ], 100, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent all customer-facing retry notifications from being sent after this email.
|
||||
*
|
||||
* @param array $rule_array The raw details about the retry rule.
|
||||
* @param int $retry_number The number of the retry.
|
||||
* @param int $order_id The ID of the order that needs payment.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function prevent_retry_notification_email( $rule_array, $retry_number, $order_id ) {
|
||||
if ( wcs_get_objects_property( $this->object, 'id' ) === $order_id ) {
|
||||
$rule_array['email_template_customer'] = '';
|
||||
}
|
||||
|
||||
return $rule_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send store owner a different email when the retry is related to an authentication required error.
|
||||
*
|
||||
* @param array $rule_array The raw details about the retry rule.
|
||||
* @param int $retry_number The number of the retry.
|
||||
* @param int $order_id The ID of the order that needs payment.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function set_store_owner_custom_email( $rule_array, $retry_number, $order_id ) {
|
||||
if (
|
||||
wcs_get_objects_property( $this->object, 'id' ) === $order_id &&
|
||||
'' !== $rule_array['email_template_admin'] // Only send our email if a retry admin email was already going to be sent.
|
||||
) {
|
||||
$rule_array['email_template_admin'] = 'WC_Payments_Email_Failed_Authentication_Retry';
|
||||
}
|
||||
|
||||
return $rule_array;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin email about payment retry failed due to authentication.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the email header.
|
||||
*/
|
||||
do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
|
||||
|
||||
<p>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
// translators: %1$s: an order number, %2$s: the customer's full name, %3$s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours'.
|
||||
_x(
|
||||
'The automatic recurring payment for order %1$s from %2$s has failed. The customer was sent an email requesting authentication of payment. If the customer does not authenticate the payment, they will be requested by email again %3$s.',
|
||||
'In admin renewal failed email',
|
||||
'woocommerce-payments'
|
||||
),
|
||||
$order->get_order_number(),
|
||||
$order->get_formatted_billing_full_name(),
|
||||
wcs_get_human_time_diff( $retry->get_time() )
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<p><?php esc_html_e( 'The renewal order is as follows:', 'woocommerce-payments' ); ?></p>
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shows the order details table.
|
||||
*/
|
||||
do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
/**
|
||||
* Shows order meta data.
|
||||
*/
|
||||
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
/**
|
||||
* Shows customer details, and email address.
|
||||
*/
|
||||
do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
/**
|
||||
* Output the email footer.
|
||||
*/
|
||||
do_action( 'woocommerce_email_footer', $email );
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* Customer email about payment retry failed due to authentication.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
?>
|
||||
|
||||
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
|
||||
|
||||
<p>
|
||||
<?php
|
||||
// translators: %1$s: name of the blog, %2$s: link to payment re-authentication URL, note: no full stop due to url at the end.
|
||||
echo wp_kses( sprintf( _x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please login and authorize the renewal from your account page: %2$s', 'In failed renewal authentication email', 'woocommerce-payments' ), esc_html( get_bloginfo( 'name' ) ), '<a href="' . esc_url( $authorization_url ) . '">' . esc_html__( 'Authorize the payment »', 'woocommerce-payments' ) . '</a>' ), [ 'a' => [ 'href' => true ] ] );
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
|
||||
|
||||
<?php do_action( 'woocommerce_email_footer', $email ); ?>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin email about payment retry failed due to authentication.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
echo '= ' . $email_heading . " =\n\n";
|
||||
|
||||
printf(
|
||||
// translators: %1$s: an order number, %2$s: the customer's full name, %3$s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours'.
|
||||
_x(
|
||||
'The automatic recurring payment for order %1$s from %2$s has failed. The customer was sent an email requesting authentication of payment. If the customer does not authenticate the payment, they will be requested by email again %3$s.',
|
||||
'In admin renewal failed email',
|
||||
'woocommerce-payments'
|
||||
),
|
||||
$order->get_order_number(),
|
||||
$order->get_formatted_billing_full_name(),
|
||||
wcs_get_human_time_diff( $retry->get_time() )
|
||||
) . "\n\n";
|
||||
printf( __( 'The renewal order is as follows:', 'woocommerce-payments' ) ) . "\n\n";
|
||||
|
||||
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
|
||||
|
||||
/**
|
||||
* Shows the order details table.
|
||||
*/
|
||||
do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
|
||||
|
||||
/**
|
||||
* Shows order meta data.
|
||||
*/
|
||||
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
/**
|
||||
* Shows customer details, and email address.
|
||||
*/
|
||||
do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
|
||||
|
||||
echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) );
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Customer email about payment retry failed due to authentication.
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
echo $email_heading . "\n\n";
|
||||
|
||||
// translators: %1$s: name of the blog, %2$s: link to checkout payment url, note: no full stop due to url at the end.
|
||||
printf( esc_html_x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please login and authorize the renewal from your account page: %2$s', 'In failed renewal authentication email', 'woocommerce-payments' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( $authorization_url ) );
|
||||
|
||||
echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
|
||||
|
||||
do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email );
|
||||
|
||||
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
|
||||
|
||||
echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) );
|
||||
+1024
File diff suppressed because it is too large
Load Diff
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait WC_Payments_Subscriptions_Utilities
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions related to WC Subscriptions.
|
||||
*/
|
||||
trait WC_Payments_Subscriptions_Utilities {
|
||||
|
||||
/**
|
||||
* Checks if subscriptions are enabled on the site.
|
||||
*
|
||||
* Subscriptions functionality is enabled if the WC Subscriptions plugin is active and greater than v 2.2, or the base feature is turned on.
|
||||
*
|
||||
* @return bool Whether subscriptions is enabled or not.
|
||||
*/
|
||||
public function is_subscriptions_enabled() {
|
||||
if ( $this->is_subscriptions_plugin_active() ) {
|
||||
return version_compare( $this->get_subscriptions_plugin_version(), '2.2.0', '>=' );
|
||||
}
|
||||
|
||||
// TODO update this once we know how the base library feature will be enabled.
|
||||
return class_exists( 'WC_Subscriptions_Core_Plugin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this user is changing the payment method for a subscription.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_changing_payment_method_for_subscription() {
|
||||
if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
return wcs_is_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean value indicating whether payment for an order will be recurring,
|
||||
* as opposed to single.
|
||||
*
|
||||
* @param int $order_id ID for corresponding WC_Order in process.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_payment_recurring( $order_id ) {
|
||||
if ( ! $this->is_subscriptions_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
return $this->is_changing_payment_method_for_subscription() || wcs_order_contains_subscription( $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether the save payment checkbox should be
|
||||
* displayed during checkout.
|
||||
*
|
||||
* Returns `false` if the cart currently has a subscriptions or if the request has a
|
||||
* `change_payment_method` GET parameter. Returns the value in `$display` otherwise.
|
||||
*
|
||||
* @param bool $display Bool indicating whether to show the save payment checkbox in the absence of subscriptions.
|
||||
*
|
||||
* @return bool Indicates whether the save payment method checkbox should be displayed or not.
|
||||
*/
|
||||
public function display_save_payment_method_checkbox( $display ) {
|
||||
if ( WC_Subscriptions_Cart::cart_contains_subscription() || $this->is_changing_payment_method_for_subscription() ) {
|
||||
return false;
|
||||
}
|
||||
// Only render the "Save payment method" checkbox if there are no subscription products in the cart.
|
||||
return $display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean on whether current WC_Cart or WC_Subscriptions_Cart
|
||||
* contains a subscription or subscription renewal item
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_subscription_item_in_cart() {
|
||||
if ( $this->is_subscriptions_enabled() ) {
|
||||
return WC_Subscriptions_Cart::cart_contains_subscription() || $this->cart_contains_renewal();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart to see if it contains a subscription product renewal.
|
||||
*
|
||||
* @return mixed The cart item containing the renewal as an array, else false.
|
||||
*/
|
||||
public function cart_contains_renewal() {
|
||||
if ( ! function_exists( 'wcs_cart_contains_renewal' ) ) {
|
||||
return false;
|
||||
}
|
||||
return wcs_cart_contains_renewal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the WC Subscriptions plugin is active.
|
||||
*
|
||||
* @return bool Whether the plugin is active or not.
|
||||
*/
|
||||
public function is_subscriptions_plugin_active() {
|
||||
return class_exists( 'WC_Subscriptions' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of WooCommerce Subscriptions that is active.
|
||||
*
|
||||
* @return null|string The plugin version. Returns null when WC Subscriptions is not active/loaded.
|
||||
*/
|
||||
public function get_subscriptions_plugin_version() {
|
||||
return class_exists( 'WC_Subscriptions' ) ? WC_Subscriptions::$version : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the subscriptions-core library.
|
||||
*
|
||||
* @return null|string The version number of subscriptions-core or null if not active.
|
||||
*/
|
||||
public function get_subscriptions_core_version() {
|
||||
$subscriptions_core_instance = WC_Subscriptions_Core_Plugin::instance();
|
||||
|
||||
// For backwards compatibility with older versions of WC Subscriptions, we need to do an existence check.
|
||||
if ( method_exists( $subscriptions_core_instance, 'get_library_version' ) ) {
|
||||
return $subscriptions_core_instance->get_library_version();
|
||||
}
|
||||
return $subscriptions_core_instance ? $subscriptions_core_instance->get_plugin_version() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of subscriptions that have already been migrated.
|
||||
*
|
||||
* @return int The total number of subscriptions migrated.
|
||||
*/
|
||||
public function get_subscription_migrated_count() {
|
||||
if ( ! function_exists( 'wcs_get_orders_with_meta_query' ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return count(
|
||||
wcs_get_orders_with_meta_query(
|
||||
[
|
||||
'status' => 'any',
|
||||
'return' => 'ids',
|
||||
'type' => 'shop_subscription',
|
||||
'limit' => -1,
|
||||
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
[
|
||||
'key' => '_migrated_wcpay_subscription_id',
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Base_Constant
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
use ReflectionClass;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base constant class to hold common logic for all constants.
|
||||
*/
|
||||
abstract class Base_Constant implements \JsonSerializable {
|
||||
|
||||
/**
|
||||
* Enum value
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Static objects cache.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $object_cache = [];
|
||||
|
||||
/**
|
||||
* Class constructor. Keep it private to only allow initializing from __callStatic()
|
||||
*
|
||||
* @param string $value Constant from class.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
private function __construct( string $value ) {
|
||||
if ( $value instanceof static ) {
|
||||
$value = $value->get_value();
|
||||
} elseif ( ! defined( static::class . "::$value" ) ) {
|
||||
throw new \InvalidArgumentException( "Constant with name '$value' does not exist." );
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enum class value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_value() {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare to enums.
|
||||
*
|
||||
* @param mixed $variable Constant object to compare.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
final public function equals( $variable = null ): bool {
|
||||
return $this === $variable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find constant in class by value.
|
||||
*
|
||||
* @param string $value Value to find.
|
||||
*
|
||||
* @return int|string
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function search( string $value ) {
|
||||
$class = new ReflectionClass( static::class );
|
||||
$key = array_search( $value, $class->getConstants(), true );
|
||||
if ( false === $key ) {
|
||||
throw new \InvalidArgumentException( "Constant with value '$value' does not exist." );
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to created enum from constant names like CLASS::ConstantName().
|
||||
*
|
||||
* @param string $name Name of property or function.
|
||||
* @param array $arguments Arguments of static call.
|
||||
*
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function __callStatic( $name, $arguments ) {
|
||||
if ( ! isset( static::$object_cache[ $name ] ) ) {
|
||||
// Instantiating constants by class name using the 'new static($name)' approach is integral to this method's functionality.
|
||||
// @phpstan-ignore-next-line.
|
||||
static::$object_cache[ $name ] = new static( $name );
|
||||
}
|
||||
return static::$object_cache[ $name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real enum value.
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function __toString() {
|
||||
return constant( \get_class( $this ) . '::' . $this->get_value() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the value which should be serialized to JSON.
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize() {
|
||||
return $this->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Country_Code
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Country Code constants
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class Country_Code extends Base_Constant {
|
||||
const AFGHANISTAN = 'AF';
|
||||
const ALAND_ISLANDS = 'AX';
|
||||
const ALBANIA = 'AL';
|
||||
const ALGERIA = 'DZ';
|
||||
const AMERICAN_SAMOA = 'AS';
|
||||
const ANDORRA = 'AD';
|
||||
const ANGOLA = 'AO';
|
||||
const ANGUILLA = 'AI';
|
||||
const ANTARCTICA = 'AQ';
|
||||
const ANTIGUA_AND_BARBUDA = 'AG';
|
||||
const ARGENTINA = 'AR';
|
||||
const ARMENIA = 'AM';
|
||||
const ARUBA = 'AW';
|
||||
const AUSTRALIA = 'AU';
|
||||
const AUSTRIA = 'AT';
|
||||
const AZERBAIJAN = 'AZ';
|
||||
const BAHAMAS = 'BS';
|
||||
const BAHRAIN = 'BH';
|
||||
const BANGLADESH = 'BD';
|
||||
const BARBADOS = 'BB';
|
||||
const BELARUS = 'BY';
|
||||
const BELGIUM = 'BE';
|
||||
const BELIZE = 'BZ';
|
||||
const BENIN = 'BJ';
|
||||
const BERMUDA = 'BM';
|
||||
const BHUTAN = 'BT';
|
||||
const BOLIVIA = 'BO';
|
||||
const BOSNIA_AND_HERZEGOVINA = 'BA';
|
||||
const BOTSWANA = 'BW';
|
||||
const BOUVET_ISLAND = 'BV';
|
||||
const BRAZIL = 'BR';
|
||||
const BRITISH_INDIAN_OCEAN_TERRITORY = 'IO';
|
||||
const BRUNEI = 'BN';
|
||||
const BULGARIA = 'BG';
|
||||
const BURKINA_FASO = 'BF';
|
||||
const BURUNDI = 'BI';
|
||||
const CABO_VERDE = 'CV';
|
||||
const CAMBODIA = 'KH';
|
||||
const CAMEROON = 'CM';
|
||||
const CANADA = 'CA';
|
||||
const CARIBBEAN_NETHERLANDS = 'BQ';
|
||||
const CENTRAL_AFRICAN_REPUBLIC = 'CF';
|
||||
const CHAD = 'TD';
|
||||
const CHILE = 'CL';
|
||||
const CHINA = 'CN';
|
||||
const COCOS_KEELING_ISLANDS = 'CC';
|
||||
const COLOMBIA = 'CO';
|
||||
const COOK_ISLANDS = 'CK';
|
||||
const COMOROS = 'KM';
|
||||
const CONGO = 'CG';
|
||||
const COSTA_RICA = 'CR';
|
||||
const CROATIA = 'HR';
|
||||
const CUBA = 'CU';
|
||||
const CYPRUS = 'CY';
|
||||
const CZECHIA = 'CZ';
|
||||
const DEMOCRATIC_REPUBLIC_OF_THE_CONGO = 'CD';
|
||||
const DENMARK = 'DK';
|
||||
const DJIBOUTI = 'DJ';
|
||||
const DOMINICA = 'DM';
|
||||
const DOMINICAN_REPUBLIC = 'DO';
|
||||
const EAST_TIMOR = 'TL';
|
||||
const ECUADOR = 'EC';
|
||||
const EGYPT = 'EG';
|
||||
const EL_SALVADOR = 'SV';
|
||||
const EQUATORIAL_GUINEA = 'GQ';
|
||||
const ERITREA = 'ER';
|
||||
const ESTONIA = 'EE';
|
||||
const ESWATINI = 'SZ';
|
||||
const ETHIOPIA = 'ET';
|
||||
const FIJI = 'FJ';
|
||||
const FINLAND = 'FI';
|
||||
const FRANCE = 'FR';
|
||||
const GABON = 'GA';
|
||||
const GAMBIA = 'GM';
|
||||
const GEORGIA = 'GE';
|
||||
const GERMANY = 'DE';
|
||||
const GIBRALTAR = 'GI';
|
||||
const GHANA = 'GH';
|
||||
const GREECE = 'GR';
|
||||
const GRENADA = 'GD';
|
||||
const GUATEMALA = 'GT';
|
||||
const GUINEA = 'GN';
|
||||
const GUINEA_BISSAU = 'GW';
|
||||
const GUYANA = 'GY';
|
||||
const HAITI = 'HT';
|
||||
const HONDURAS = 'HN';
|
||||
const HONG_KONG = 'HK';
|
||||
const HUNGARY = 'HU';
|
||||
const ICELAND = 'IS';
|
||||
const INDIA = 'IN';
|
||||
const INDONESIA = 'ID';
|
||||
const IRAN = 'IR';
|
||||
const IRAQ = 'IQ';
|
||||
const IRELAND = 'IE';
|
||||
const ISRAEL = 'IL';
|
||||
const ITALY = 'IT';
|
||||
const IVORY_COAST = 'CI';
|
||||
const JAMAICA = 'JM';
|
||||
const JAPAN = 'JP';
|
||||
const JORDAN = 'JO';
|
||||
const KAZAKHSTAN = 'KZ';
|
||||
const KENYA = 'KE';
|
||||
const KIRIBATI = 'KI';
|
||||
const KOSOVO = 'XK';
|
||||
const KUWAIT = 'KW';
|
||||
const KYRGYZSTAN = 'KG';
|
||||
const LAOS = 'LA';
|
||||
const LATVIA = 'LV';
|
||||
const LEBANON = 'LB';
|
||||
const LESOTHO = 'LS';
|
||||
const LIBERIA = 'LR';
|
||||
const LIBYA = 'LY';
|
||||
const LIECHTENSTEIN = 'LI';
|
||||
const LITHUANIA = 'LT';
|
||||
const LUXEMBOURG = 'LU';
|
||||
const MADAGASCAR = 'MG';
|
||||
const MALAWI = 'MW';
|
||||
const MALAYSIA = 'MY';
|
||||
const MALDIVES = 'MV';
|
||||
const MALI = 'ML';
|
||||
const MALTA = 'MT';
|
||||
const MARSHALL_ISLANDS = 'MH';
|
||||
const MAURITANIA = 'MR';
|
||||
const MAURITIUS = 'MU';
|
||||
const MEXICO = 'MX';
|
||||
const MICRONESIA = 'FM';
|
||||
const MOLDOVA = 'MD';
|
||||
const MONACO = 'MC';
|
||||
const MONGOLIA = 'MN';
|
||||
const MONTENEGRO = 'ME';
|
||||
const MOROCCO = 'MA';
|
||||
const MOZAMBIQUE = 'MZ';
|
||||
const MYANMAR = 'MM';
|
||||
const NAMIBIA = 'NA';
|
||||
const NAURU = 'NR';
|
||||
const NEPAL = 'NP';
|
||||
const NETHERLANDS = 'NL';
|
||||
const NEW_ZEALAND = 'NZ';
|
||||
const NICARAGUA = 'NI';
|
||||
const NIGER = 'NE';
|
||||
const NIGERIA = 'NG';
|
||||
const NORTH_KOREA = 'KP';
|
||||
const NORTH_MACEDONIA = 'MK';
|
||||
const NORWAY = 'NO';
|
||||
const OMAN = 'OM';
|
||||
const PAKISTAN = 'PK';
|
||||
const PALAU = 'PW';
|
||||
const PALESTINE = 'PS';
|
||||
const PANAMA = 'PA';
|
||||
const PAPUA_NEW_GUINEA = 'PG';
|
||||
const PARAGUAY = 'PY';
|
||||
const PERU = 'PE';
|
||||
const PHILIPPINES = 'PH';
|
||||
const POLAND = 'PL';
|
||||
const PORTUGAL = 'PT';
|
||||
const PUERTO_RICO = 'PR';
|
||||
const QATAR = 'QA';
|
||||
const ROMANIA = 'RO';
|
||||
const RUSSIA = 'RU';
|
||||
const RWANDA = 'RW';
|
||||
const SAINT_BARTHELEMY = 'BL';
|
||||
const SAINT_KITTS_AND_NEVIS = 'KN';
|
||||
const SAINT_LUCIA = 'LC';
|
||||
const SAINT_VINCENT_AND_THE_GRENADINES = 'VC';
|
||||
const SAMOA = 'WS';
|
||||
const SAN_MARINO = 'SM';
|
||||
const SAO_TOME_AND_PRINCIPE = 'ST';
|
||||
const SAUDI_ARABIA = 'SA';
|
||||
const SENEGAL = 'SN';
|
||||
const SERBIA = 'RS';
|
||||
const SEYCHELLES = 'SC';
|
||||
const SIERRA_LEONE = 'SL';
|
||||
const SINGAPORE = 'SG';
|
||||
const SLOVAKIA = 'SK';
|
||||
const SLOVENIA = 'SI';
|
||||
const SOLOMON_ISLANDS = 'SB';
|
||||
const SOMALIA = 'SO';
|
||||
const SOUTH_AFRICA = 'ZA';
|
||||
const SOUTH_KOREA = 'KR';
|
||||
const SOUTH_SUDAN = 'SS';
|
||||
const SPAIN = 'ES';
|
||||
const SRI_LANKA = 'LK';
|
||||
const SUDAN = 'SD';
|
||||
const SURINAME = 'SR';
|
||||
const SWEDEN = 'SE';
|
||||
const SWITZERLAND = 'CH';
|
||||
const SYRIA = 'SY';
|
||||
const TAIWAN = 'TW';
|
||||
const TAJIKISTAN = 'TJ';
|
||||
const TANZANIA = 'TZ';
|
||||
const THAILAND = 'TH';
|
||||
const TOGO = 'TG';
|
||||
const TONGA = 'TO';
|
||||
const TRINIDAD_AND_TOBAGO = 'TT';
|
||||
const TUNISIA = 'TN';
|
||||
const TURKEY = 'TR';
|
||||
const TURKMENISTAN = 'TM';
|
||||
const TUVALU = 'TV';
|
||||
const UGANDA = 'UG';
|
||||
const UKRAINE = 'UA';
|
||||
const UNITED_ARAB_EMIRATES = 'AE';
|
||||
const UNITED_KINGDOM = 'GB';
|
||||
const UNITED_STATES = 'US';
|
||||
const URUGUAY = 'UY';
|
||||
const UZBEKISTAN = 'UZ';
|
||||
const VANUATU = 'VU';
|
||||
const VATICAN_CITY = 'VA';
|
||||
const VENEZUELA = 'VE';
|
||||
const VIETNAM = 'VN';
|
||||
const YEMEN = 'YE';
|
||||
const ZAMBIA = 'ZM';
|
||||
const ZIMBABWE = 'ZW';
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Country_Test_Cards
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class handling country-specific test card numbers for WooPayments
|
||||
*/
|
||||
class Country_Test_Cards extends Base_Constant {
|
||||
/**
|
||||
* Map of country codes to their test card numbers
|
||||
* Source: https://docs.stripe.com/testing?testing-method=card-numbers#international-cards
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $country_test_cards = [
|
||||
Country_Code::UNITED_STATES => '4242 4242 4242 4242',
|
||||
Country_Code::ARGENTINA => '4000 0003 2000 0021',
|
||||
Country_Code::BRAZIL => '4000 0007 6000 0002',
|
||||
Country_Code::CANADA => '4000 0012 4000 0000',
|
||||
Country_Code::CHILE => '4000 0015 2000 0001',
|
||||
Country_Code::COLOMBIA => '4000 0017 0000 0003',
|
||||
Country_Code::COSTA_RICA => '4000 0018 8000 0005',
|
||||
Country_Code::ECUADOR => '4000 0021 8000 0000',
|
||||
Country_Code::MEXICO => '4000 0048 4000 8001',
|
||||
Country_Code::PANAMA => '4000 0059 1000 0000',
|
||||
Country_Code::PARAGUAY => '4000 0060 0000 0066',
|
||||
Country_Code::PERU => '4000 0060 4000 0068',
|
||||
Country_Code::URUGUAY => '4000 0085 8000 0003',
|
||||
Country_Code::UNITED_ARAB_EMIRATES => '4000 0078 4000 0001',
|
||||
Country_Code::AUSTRIA => '4000 0004 0000 0008',
|
||||
Country_Code::BELGIUM => '4000 0005 6000 0004',
|
||||
Country_Code::BULGARIA => '4000 0010 0000 0000',
|
||||
Country_Code::BELARUS => '4000 0011 2000 0005',
|
||||
Country_Code::CROATIA => '4000 0019 1000 0009',
|
||||
Country_Code::CYPRUS => '4000 0019 6000 0008',
|
||||
Country_Code::CZECHIA => '4000 0020 3000 0002',
|
||||
Country_Code::DENMARK => '4000 0020 8000 0001',
|
||||
Country_Code::ESTONIA => '4000 0023 3000 0009',
|
||||
Country_Code::FINLAND => '4000 0024 6000 0001',
|
||||
Country_Code::FRANCE => '4000 0025 0000 0003',
|
||||
Country_Code::GERMANY => '4000 0027 6000 0016',
|
||||
Country_Code::GIBRALTAR => '4000 0029 2000 0005',
|
||||
Country_Code::GREECE => '4000 0030 0000 0030',
|
||||
Country_Code::HUNGARY => '4000 0034 8000 0005',
|
||||
Country_Code::IRELAND => '4000 0037 2000 0005',
|
||||
Country_Code::ITALY => '4000 0038 0000 0008',
|
||||
Country_Code::LATVIA => '4000 0042 8000 0005',
|
||||
Country_Code::LIECHTENSTEIN => '4000 0043 8000 0004',
|
||||
Country_Code::LITHUANIA => '4000 0044 0000 0000',
|
||||
Country_Code::LUXEMBOURG => '4000 0044 2000 0006',
|
||||
Country_Code::MALTA => '4000 0047 0000 0007',
|
||||
Country_Code::NETHERLANDS => '4000 0052 8000 0002',
|
||||
Country_Code::NORWAY => '4000 0057 8000 0007',
|
||||
Country_Code::POLAND => '4000 0061 6000 0005',
|
||||
Country_Code::PORTUGAL => '4000 0062 0000 0007',
|
||||
Country_Code::ROMANIA => '4000 0064 2000 0001',
|
||||
Country_Code::SAUDI_ARABIA => '4000 0068 2000 0007',
|
||||
Country_Code::SLOVENIA => '4000 0070 5000 0006',
|
||||
Country_Code::SLOVAKIA => '4000 0070 3000 0001',
|
||||
Country_Code::SPAIN => '4000 0072 4000 0007',
|
||||
Country_Code::SWEDEN => '4000 0075 2000 0008',
|
||||
Country_Code::SWITZERLAND => '4000 0075 6000 0009',
|
||||
Country_Code::UNITED_KINGDOM => '4000 0082 6000 0000',
|
||||
Country_Code::AUSTRALIA => '4000 0003 6000 0006',
|
||||
Country_Code::CHINA => '4000 0015 6000 0002',
|
||||
Country_Code::HONG_KONG => '4000 0034 4000 0004',
|
||||
Country_Code::INDIA => '4000 0035 6000 0008',
|
||||
Country_Code::JAPAN => '4000 0039 2000 0003',
|
||||
Country_Code::MALAYSIA => '4000 0045 8000 0002',
|
||||
Country_Code::NEW_ZEALAND => '4000 0055 4000 0008',
|
||||
Country_Code::SINGAPORE => '4000 0070 2000 0003',
|
||||
Country_Code::TAIWAN => '4000 0015 8000 0008',
|
||||
Country_Code::THAILAND => '4000 0076 4000 0003',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get test card number for a specific country.
|
||||
*
|
||||
* @param string $country_code Two-letter country code.
|
||||
* @return string Test card number
|
||||
*/
|
||||
public static function get_test_card_for_country( string $country_code ) {
|
||||
return self::$country_test_cards[ $country_code ] ?? self::$country_test_cards[ Country_Code::UNITED_STATES ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Currency_Code
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency Code constants
|
||||
*
|
||||
* @psalm-immutale
|
||||
*/
|
||||
class Currency_Code extends Base_Constant {
|
||||
const UNITED_STATES_DOLLAR = 'USD'; // United States Dollar.
|
||||
const UNITED_ARAB_EMIRATES_DIRHAM = 'AED'; // United Arab Emirates dirham.
|
||||
const AFGHAN_AFGHANI = 'AFN'; // Afghan afghani.
|
||||
const ALBANIAN_LEK = 'ALL'; // Albanian lek.
|
||||
const ARMENIAN_DRAM = 'AMD'; // Armenian dram.
|
||||
const NETHERLANDS_ANTILLEAN_GUILDER = 'ANG'; // Netherlands Antillean guilder.
|
||||
const ANGOLAN_KWANZA = 'AOA'; // Angolan kwanza.
|
||||
const ARGENTINE_PESO = 'ARS'; // Argentine peso.
|
||||
const AUSTRALIAN_DOLLAR = 'AUD'; // Australian dollar.
|
||||
const ARUBAN_FLORIN = 'AWG'; // Aruban florin.
|
||||
const AZERBAIJANI_MANAT = 'AZN'; // Azerbaijani manat.
|
||||
const BOSNIA_AND_HERZEGOVINA_CONVERTIBLE_MARK = 'BAM'; // Bosnia and Herzegovina convertible mark.
|
||||
const BARBADOS_DOLLAR = 'BBD'; // Barbados dollar.
|
||||
const BANGLADESHI_TAKA = 'BDT'; // Bangladeshi taka.
|
||||
const BULGARIAN_LEV = 'BGN'; // Bulgarian lev.
|
||||
const BURUNDIAN_FRANC = 'BIF'; // Burundian franc.
|
||||
const BERMUDIAN_DOLLAR = 'BMD'; // Bermudian dollar.
|
||||
const BRUNEI_DOLLAR = 'BND'; // Brunei dollar.
|
||||
const BOLIVIANO = 'BOB'; // Boliviano.
|
||||
const BRAZILIAN_REAL = 'BRL'; // Brazilian real.
|
||||
const BAHAMIAN_DOLLAR = 'BSD'; // Bahamian dollar.
|
||||
const BOTSWANA_PULA = 'BWP'; // Botswana pula.
|
||||
const NEW_BELARUSIAN_RUBLE = 'BYN'; // New Belarusian ruble.
|
||||
const BELIZE_DOLLAR = 'BZD'; // Belize dollar.
|
||||
const CANADIAN_DOLLAR = 'CAD'; // Canadian dollar.
|
||||
const CONGOLESE_FRANC = 'CDF'; // Congolese franc.
|
||||
const SWISS_FRANC = 'CHF'; // Swiss franc.
|
||||
const CHILEAN_PESO = 'CLP'; // Chilean peso.
|
||||
const CHINESE_YUAN = 'CNY'; // Renminbi (Chinese yuan).
|
||||
const COLOMBIAN_PESO = 'COP'; // Colombian peso.
|
||||
const COSTA_RICAN_COLON = 'CRC'; // Costa Rican colon.
|
||||
const CAPE_VERDE_ESCUDO = 'CVE'; // Cape Verde escudo.
|
||||
const CZECH_KORUNA = 'CZK'; // Czech koruna.
|
||||
const DJIBOUTIAN_FRANC = 'DJF'; // Djiboutian franc.
|
||||
const DANISH_KRONE = 'DKK'; // Danish krone.
|
||||
const DOMINICAN_PESO = 'DOP'; // Dominican peso.
|
||||
const ALGERIAN_DINAR = 'DZD'; // Algerian dinar.
|
||||
const EGYPTIAN_POUND = 'EGP'; // Egyptian pound.
|
||||
const ETHIOPIAN_BIRR = 'ETB'; // Ethiopian birr.
|
||||
const EURO = 'EUR'; // Euro.
|
||||
const FIJI_DOLLAR = 'FJD'; // Fiji dollar.
|
||||
const FALKLAND_ISLANDS_POUND = 'FKP'; // Falkland Islands pound.
|
||||
const POUND_STERLING = 'GBP'; // Pound sterling.
|
||||
const GEORGIAN_LARI = 'GEL'; // Georgian lari.
|
||||
const GIBRALTAR_POUND = 'GIP'; // Gibraltar pound.
|
||||
const GAMBIAN_DALASI = 'GMD'; // Gambian dalasi.
|
||||
const GUINEAN_FRANC = 'GNF'; // Guinean franc.
|
||||
const GUATEMALAN_QUETZAL = 'GTQ'; // Guatemalan quetzal.
|
||||
const GUYANESE_DOLLAR = 'GYD'; // Guyanese dollar.
|
||||
const HONG_KONG_DOLLAR = 'HKD'; // Hong Kong dollar.
|
||||
const HONDURAN_LEMPIRA = 'HNL'; // Honduran lempira.
|
||||
const HAITIAN_GOURDE = 'HTG'; // Haitian gourde.
|
||||
const HUNGARIAN_FORINT = 'HUF'; // Hungarian forint.
|
||||
const INDONESIAN_RUPIAH = 'IDR'; // Indonesian rupiah.
|
||||
const ISRAELI_NEW_SHEKEL = 'ILS'; // Israeli new shekel.
|
||||
const INDIAN_RUPEE = 'INR'; // Indian rupee.
|
||||
const ICELANDIC_KRONA = 'ISK'; // Icelandic króna.
|
||||
const JAMAICAN_DOLLAR = 'JMD'; // Jamaican dollar.
|
||||
const JAPANESE_YEN = 'JPY'; // Japanese yen.
|
||||
const KENYAN_SHILLING = 'KES'; // Kenyan shilling.
|
||||
const KYRGYZSTANI_SOM = 'KGS'; // Kyrgyzstani som.
|
||||
const CAMBODIAN_RIEL = 'KHR'; // Cambodian riel.
|
||||
const COMORIAN_FRANC = 'KMF'; // Comorian franc.
|
||||
const SOUTH_KOREAN_WON = 'KRW'; // South Korean won.
|
||||
const CAYMAN_ISLANDS_DOLLAR = 'KYD'; // Cayman Islands dollar.
|
||||
const KAZAKHSTANI_TENGE = 'KZT'; // Kazakhstani tenge.
|
||||
const LAO_KIP = 'LAK'; // Lao kip.
|
||||
const LEBANESE_POUND = 'LBP'; // Lebanese pound.
|
||||
const SRI_LANKAN_RUPEE = 'LKR'; // Sri Lankan rupee.
|
||||
const LIBERIAN_DOLLAR = 'LRD'; // Liberian dollar.
|
||||
const LESOTHO_LOTI = 'LSL'; // Lesotho loti.
|
||||
const MOROCCAN_DIRHAM = 'MAD'; // Moroccan dirham.
|
||||
const MOLDOVAN_LEU = 'MDL'; // Moldovan leu.
|
||||
const MALAGASY_ARIARY = 'MGA'; // Malagasy ariary.
|
||||
const MACEDONIAN_DENAR = 'MKD'; // Macedonian denar.
|
||||
const MYANMAR_KYAT = 'MMK'; // Myanmar kyat.
|
||||
const MONGOLIAN_TOGROG = 'MNT'; // Mongolian tögrög.
|
||||
const MACANESE_PATACA = 'MOP'; // Macanese pataca.
|
||||
const MAURITIAN_RUPEE = 'MUR'; // Mauritian rupee.
|
||||
const MALDIVIAN_RUFIYAA = 'MVR'; // Maldivian rufiyaa.
|
||||
const MALAWIAN_KWACHA = 'MWK'; // Malawian kwacha.
|
||||
const MEXICAN_PESO = 'MXN'; // Mexican peso.
|
||||
const MALAYSIAN_RINGGIT = 'MYR'; // Malaysian ringgit.
|
||||
const MOZAMBICAN_METICAL = 'MZN'; // Mozambican metical.
|
||||
const NAMIBIAN_DOLLAR = 'NAD'; // Namibian dollar.
|
||||
const NIGERIAN_NAIRA = 'NGN'; // Nigerian naira.
|
||||
const NICARAGUAN_CORDOBA = 'NIO'; // Nicaraguan córdoba.
|
||||
const NORWEGIAN_KRONE = 'NOK'; // Norwegian krone.
|
||||
const NEPALESE_RUPEE = 'NPR'; // Nepalese rupee.
|
||||
const NEW_ZEALAND_DOLLAR = 'NZD'; // New Zealand dollar.
|
||||
const PANAMANIAN_BALBOA = 'PAB'; // Panamanian balboa.
|
||||
const PERUVIAN_SOL = 'PEN'; // Peruvian sol.
|
||||
const PAPUA_NEW_GUINEAN_KINA = 'PGK'; // Papua New Guinean kina.
|
||||
const PHILIPPINE_PESO = 'PHP'; // Philippine peso.
|
||||
const PAKISTANI_RUPEE = 'PKR'; // Pakistani rupee.
|
||||
const POLISH_ZLOTY = 'PLN'; // Polish złoty.
|
||||
const PARAGUAYAN_GUARANI = 'PYG'; // Paraguayan guaraní.
|
||||
const QATARI_RIYAL = 'QAR'; // Qatari riyal.
|
||||
const ROMANIAN_LEU = 'RON'; // Romanian leu.
|
||||
const SERBIAN_DINAR = 'RSD'; // Serbian dinar.
|
||||
const RUSSIAN_RUBLE = 'RUB'; // Russian ruble.
|
||||
const RWANDAN_FRANC = 'RWF'; // Rwandan franc.
|
||||
const SAUDI_RIYAL = 'SAR'; // Saudi riyal.
|
||||
const SOLOMON_ISLANDS_DOLLAR = 'SBD'; // Solomon Islands dollar.
|
||||
const SEYCHELLOIS_RUPEE = 'SCR'; // Seychellois rupee.
|
||||
const SWEDISH_KRONA = 'SEK'; // Swedish krona.
|
||||
const SINGAPORE_DOLLAR = 'SGD'; // Singapore dollar.
|
||||
const SAINT_HELENA_POUND = 'SHP'; // Saint Helena pound.
|
||||
const SIERRA_LEONEAN_LEONE = 'SLE'; // Sierra Leonean leone.
|
||||
const SOMALI_SHILLING = 'SOS'; // Somali shilling.
|
||||
const SURINAMESE_DOLLAR = 'SRD'; // Surinamese dollar.
|
||||
const SAO_TOME_AND_PRINCIPE_DOBRA = 'STD'; // São Tomé and Príncipe dobra.
|
||||
const SWAZI_LILANGENI = 'SZL'; // Swazi lilangeni.
|
||||
const THAI_BAHT = 'THB'; // Thai baht.
|
||||
const TAJIKISTANI_SOMONI = 'TJS'; // Tajikistani somoni.
|
||||
const TONGAN_PAANGA = 'TOP'; // Tongan paʻanga.
|
||||
const TURKISH_LIRA = 'TRY'; // Turkish lira.
|
||||
const TRINIDAD_AND_TOBAGO_DOLLAR = 'TTD'; // Trinidad and Tobago dollar.
|
||||
const NEW_TAIWAN_DOLLAR = 'TWD'; // New Taiwan dollar.
|
||||
const TANZANIAN_SHILLING = 'TZS'; // Tanzanian shilling.
|
||||
const UKRAINIAN_HRYVNIA = 'UAH'; // Ukrainian hryvnia.
|
||||
const UGANDAN_SHILLING = 'UGX'; // Ugandan shilling.
|
||||
const URUGUAYAN_PESO = 'UYU'; // Uruguayan peso.
|
||||
const UZBEKISTANI_SOM = 'UZS'; // Uzbekistani som.
|
||||
const VIETNAMESE_DONG = 'VND'; // Vietnamese đồng.
|
||||
const VANUATU_VATU = 'VUV'; // Vanuatu vatu.
|
||||
const SAMOAN_TALA = 'WST'; // Samoan tala.
|
||||
const CENTRAL_AFRICAN_CFA_FRANC = 'XAF'; // Central African CFA franc.
|
||||
const EAST_CARIBBEAN_DOLLAR = 'XCD'; // East Caribbean dollar.
|
||||
const WEST_AFRICAN_CFA_FRANC = 'XOF'; // West African CFA franc.
|
||||
const CFP_FRANC = 'XPF'; // CFP franc.
|
||||
const YEMENI_RIAL = 'YER'; // Yemeni rial.
|
||||
const SOUTH_AFRICAN_RAND = 'ZAR'; // South African rand.
|
||||
const ZAMBIAN_KWACHA = 'ZMW'; // Zambian kwacha.
|
||||
// ... add more currencies as needed.
|
||||
// crypto currencies.
|
||||
const BITCOIN = 'BTC'; // Bitcoin.
|
||||
}
|
||||
+1162
File diff suppressed because it is too large
Load Diff
+360
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Express_Checkout_Hong_Kong_States
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Express_Checkout_Hong_Kong_States
|
||||
*
|
||||
* Contains a list of districts (equivalent to WC states) in Hong Kong used for normalization and validation purposes.
|
||||
* This is necessary due to a bug in Apple Pay that's currently being worked on. Until
|
||||
* that bug is fixed this workaround will be necessary.
|
||||
*
|
||||
* More info in pc4etw-bY-p2.
|
||||
*
|
||||
* Note: The following code has been kindly sourced from the WooCommerce Stripe Payment Gateway implementation,
|
||||
* which addresses the same issue in that plugin. See https://github.com/woocommerce/woocommerce-gateway-stripe/pull/1593.
|
||||
*
|
||||
* @since x.x.x
|
||||
*/
|
||||
class Express_Checkout_Hong_Kong_States {
|
||||
// Source: https://www.rvd.gov.hk/doc/tc/hkpr13/06.pdf.
|
||||
const STATES = [
|
||||
'hong kong',
|
||||
'hong kong island',
|
||||
'港島',
|
||||
|
||||
'central and western',
|
||||
'中西區',
|
||||
|
||||
'kennedy town',
|
||||
'shek tong tsui',
|
||||
'sai ying pun',
|
||||
'sheung wan',
|
||||
'central',
|
||||
'admiralty',
|
||||
'mid-levels',
|
||||
'peak',
|
||||
'堅尼地城',
|
||||
'石塘咀',
|
||||
'西營盤',
|
||||
'上環',
|
||||
'中環',
|
||||
'金鐘',
|
||||
'半山區',
|
||||
'山頂',
|
||||
|
||||
'wan chai',
|
||||
'灣仔',
|
||||
|
||||
'causeway bay',
|
||||
'happy valley',
|
||||
'tai hang',
|
||||
'so kon po',
|
||||
"jardine's lookout",
|
||||
'灣仔',
|
||||
'銅鑼灣',
|
||||
'跑馬地',
|
||||
'大坑',
|
||||
'掃桿埔',
|
||||
'渣甸山',
|
||||
|
||||
'eastern',
|
||||
'東區',
|
||||
|
||||
'tin hau',
|
||||
'braemar hill',
|
||||
'north point',
|
||||
'quarry bay',
|
||||
'sai wan ho',
|
||||
'shau kei wan',
|
||||
'chai wan',
|
||||
'siu sai wan',
|
||||
'天后',
|
||||
'寶馬山',
|
||||
'北角',
|
||||
'鰂魚涌',
|
||||
'西灣河',
|
||||
'筲箕灣',
|
||||
'柴灣',
|
||||
'小西灣',
|
||||
|
||||
'southern',
|
||||
'南區',
|
||||
|
||||
'pok fu lam',
|
||||
'aberdeen',
|
||||
'ap lei chau',
|
||||
'wong chuk hang',
|
||||
'shouson hill',
|
||||
'repulse bay',
|
||||
'chung hom kok',
|
||||
'stanley',
|
||||
'tai tam',
|
||||
'shek o',
|
||||
'薄扶林',
|
||||
'香港仔',
|
||||
'鴨脷洲',
|
||||
'黃竹坑',
|
||||
'壽臣山',
|
||||
'淺水灣',
|
||||
'舂磡角',
|
||||
'赤柱',
|
||||
'大潭',
|
||||
'石澳',
|
||||
|
||||
'kowloon',
|
||||
'九龍',
|
||||
|
||||
'yau tsim mong',
|
||||
'油尖旺',
|
||||
|
||||
'tsim sha tsui',
|
||||
'yau ma tei',
|
||||
'west kowloon reclamation',
|
||||
"king's park, mong kok",
|
||||
'tai kok tsui',
|
||||
'尖沙咀',
|
||||
'油麻地',
|
||||
'西九龍填海區',
|
||||
'京士柏',
|
||||
'旺角',
|
||||
'大角咀',
|
||||
|
||||
'sham shui po',
|
||||
'深水埗',
|
||||
|
||||
'mei foo',
|
||||
'lai chi kok',
|
||||
'cheung sha wan',
|
||||
'shek kip mei',
|
||||
'yau yat tsuen',
|
||||
'tai wo ping',
|
||||
'stonecutters island',
|
||||
'美孚',
|
||||
'荔枝角',
|
||||
'長沙灣',
|
||||
'石硤尾',
|
||||
'又一村',
|
||||
'大窩坪',
|
||||
'昂船洲',
|
||||
|
||||
'kowloon city',
|
||||
'九龍城',
|
||||
|
||||
'hung hom',
|
||||
'to kwa wan',
|
||||
'ma tau kok',
|
||||
'ma tau wai',
|
||||
'kai tak',
|
||||
'ho man tin',
|
||||
'kowloon tong',
|
||||
'beacon hill',
|
||||
'紅磡',
|
||||
'土瓜灣',
|
||||
'馬頭角',
|
||||
'馬頭圍',
|
||||
'啟德',
|
||||
'何文田',
|
||||
'九龍塘',
|
||||
'筆架山',
|
||||
|
||||
'wong tai sin',
|
||||
'黃大仙',
|
||||
|
||||
'san po kong',
|
||||
'tung tau',
|
||||
'wang tau hom',
|
||||
'lok fu',
|
||||
'diamond hill',
|
||||
'tsz wan shan',
|
||||
'ngau chi wan',
|
||||
'新蒲崗',
|
||||
'東頭',
|
||||
'橫頭磡',
|
||||
'樂富',
|
||||
'鑽石山',
|
||||
'慈雲山',
|
||||
'牛池灣',
|
||||
|
||||
'kwun tong',
|
||||
'觀塘',
|
||||
|
||||
'ping shek',
|
||||
'kowloon bay',
|
||||
'ngau tau kok',
|
||||
'jordan valley',
|
||||
'kwun tong',
|
||||
'sau mau ping',
|
||||
'lam tin',
|
||||
'yau tong',
|
||||
'lei yue mun',
|
||||
'坪石',
|
||||
'九龍灣',
|
||||
'牛頭角',
|
||||
'佐敦谷',
|
||||
'觀塘',
|
||||
'秀茂坪',
|
||||
'藍田',
|
||||
'油塘',
|
||||
'鯉魚門',
|
||||
|
||||
'new territories',
|
||||
'新界',
|
||||
|
||||
'kwai tsing',
|
||||
'葵青',
|
||||
|
||||
'kwai chung',
|
||||
'tsing yi',
|
||||
'葵涌',
|
||||
'青衣',
|
||||
|
||||
'tsuen wan',
|
||||
'荃灣',
|
||||
|
||||
'lei muk shue',
|
||||
'ting kau',
|
||||
'sham tseng',
|
||||
'tsing lung tau',
|
||||
'ma wan',
|
||||
'sunny bay',
|
||||
'梨木樹',
|
||||
'汀九',
|
||||
'深井',
|
||||
'青龍頭',
|
||||
'馬灣',
|
||||
'欣澳',
|
||||
|
||||
'tuen mun',
|
||||
'屯門',
|
||||
|
||||
'tai lam chung',
|
||||
'so kwun wat',
|
||||
'tuen mun',
|
||||
'lam tei',
|
||||
'大欖涌',
|
||||
'掃管笏',
|
||||
'屯門',
|
||||
'藍地',
|
||||
|
||||
'yuen long',
|
||||
'元朗',
|
||||
|
||||
'hung shui kiu',
|
||||
'ha tsuen',
|
||||
'lau fau shan',
|
||||
'tin shui wai',
|
||||
'yuen long',
|
||||
'san tin',
|
||||
'lok ma chau',
|
||||
'kam tin',
|
||||
'shek kong',
|
||||
'pat heung',
|
||||
'洪水橋',
|
||||
'廈村',
|
||||
'流浮山',
|
||||
'天水圍',
|
||||
'元朗',
|
||||
'新田',
|
||||
'落馬洲',
|
||||
'錦田',
|
||||
'石崗',
|
||||
'八鄉',
|
||||
|
||||
'north',
|
||||
'北區',
|
||||
|
||||
'fanling',
|
||||
'luen wo hui',
|
||||
'sheung shui',
|
||||
'shek wu hui',
|
||||
'sha tau kok',
|
||||
'luk keng',
|
||||
'wu kau tang',
|
||||
'粉嶺',
|
||||
'聯和墟',
|
||||
'上水',
|
||||
'石湖墟',
|
||||
'沙頭角',
|
||||
'鹿頸',
|
||||
'烏蛟騰',
|
||||
|
||||
'tai po',
|
||||
'大埔',
|
||||
|
||||
'tai po market',
|
||||
'tai po kau',
|
||||
'tai mei tuk',
|
||||
'shuen wan',
|
||||
'cheung muk tau',
|
||||
'kei ling ha',
|
||||
'大埔墟',
|
||||
'大埔',
|
||||
'大埔滘',
|
||||
'大尾篤',
|
||||
'船灣',
|
||||
'樟木頭',
|
||||
'企嶺下',
|
||||
|
||||
'sha tin',
|
||||
'沙田',
|
||||
|
||||
'tai wai',
|
||||
'fo tan',
|
||||
'ma liu shui',
|
||||
'wu kai sha',
|
||||
'ma on shan',
|
||||
'大圍',
|
||||
'火炭',
|
||||
'馬料水',
|
||||
'烏溪沙',
|
||||
'馬鞍山',
|
||||
|
||||
'sai kung',
|
||||
'西貢',
|
||||
|
||||
'clear water bay',
|
||||
'tai mong tsai',
|
||||
'tseung kwan o',
|
||||
'hang hau',
|
||||
'tiu keng leng',
|
||||
'ma yau tong',
|
||||
'清水灣',
|
||||
'大網仔',
|
||||
'將軍澳',
|
||||
'坑口',
|
||||
'調景嶺',
|
||||
'馬游塘',
|
||||
|
||||
'islands',
|
||||
'離島',
|
||||
|
||||
'cheung chau',
|
||||
'peng chau',
|
||||
'lantau island (including tung chung)',
|
||||
'lamma island',
|
||||
'長洲',
|
||||
'坪洲',
|
||||
'大嶼山(包括東涌)',
|
||||
'南丫島',
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks if the given state is a valid region (equivalent to WC state) in Hong Kong.
|
||||
*
|
||||
* @param string $state The state to be evaluated.
|
||||
* @return bool True if the provided state is valid, false otherwise.
|
||||
*/
|
||||
public static function is_valid_state( $state ) {
|
||||
return in_array( $state, self::STATES, true );
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Fraud_Meta_Box_Type
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* This class gives a list of all the possible fraud meta box type constants.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class Fraud_Meta_Box_Type extends Base_Constant {
|
||||
const ALLOW = 'allow';
|
||||
const BLOCK = 'block';
|
||||
const NOT_CARD = 'not_card';
|
||||
const NOT_WCPAY = 'not_wcpay';
|
||||
const PAYMENT_STARTED = 'payment_started';
|
||||
const REVIEW = 'review';
|
||||
const REVIEW_ALLOWED = 'review_allowed';
|
||||
const REVIEW_BLOCKED = 'review_blocked';
|
||||
const REVIEW_EXPIRED = 'review_expired';
|
||||
const REVIEW_FAILED = 'review_failed';
|
||||
const TERMINAL_PAYMENT = 'terminal_payment';
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Intent_Status
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* This class gives a list of all Payment and Setup Intent status name constants.
|
||||
* - https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status
|
||||
* - https://stripe.com/docs/api/setup_intents/object#setup_intent_object-status
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class Intent_Status extends Base_Constant {
|
||||
const REQUIRES_PAYMENT_METHOD = 'requires_payment_method';
|
||||
const REQUIRES_CONFIRMATION = 'requires_confirmation';
|
||||
const REQUIRES_ACTION = 'requires_action';
|
||||
const PROCESSING = 'processing';
|
||||
const REQUIRES_CAPTURE = 'requires_capture';
|
||||
const CANCELED = 'canceled';
|
||||
const SUCCEEDED = 'succeeded';
|
||||
|
||||
/**
|
||||
* Stripe intents that are treated as successfully created, i.e. authorized.
|
||||
*
|
||||
* @type array
|
||||
*/
|
||||
const AUTHORIZED_STATUSES = [
|
||||
self::SUCCEEDED,
|
||||
self::REQUIRES_CAPTURE,
|
||||
self::PROCESSING,
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user