WooCommerce Code Reference

class-wc-rest-paypal-buttons-controller.php

Source code

<?php
/**
 *
 * REST API PayPal buttons controller
 *
 * @package WooCommerce\RestApi
 * @since   10.3.0
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Enums\OrderStatus;

if ( ! class_exists( 'WC_Gateway_Paypal_Constants' ) ) {
	require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-constants.php';
}

if ( ! class_exists( 'WC_Gateway_Paypal_Request' ) ) {
	require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php';
}


/**
 * REST API PayPal buttons controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Paypal_Buttons_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'paypal-buttons';

	/**
	 * Register the routes for the PayPal buttons functionality handler.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/create-order',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_order' ),
				'permission_callback' => array( $this, 'validate_create_order_request' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/cancel-payment',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'cancel_payment' ),
				'permission_callback' => array( $this, 'validate_cancel_payment_request' ),
			)
		);
	}

	/**
	 * Validate the create order request.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return bool True if the create order request is valid, false otherwise.
	 */
	public function validate_create_order_request( WP_REST_Request $request ) {
		if ( $request->get_header( 'Nonce' ) ) {
			$nonce = $request->get_header( 'Nonce' );
			return wp_verify_nonce( $nonce, 'wc_gateway_paypal_standard_create_order' );
		}
		return false;
	}

	/**
	 * Validate the cancel payment request.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return bool True if the cancel payment request is valid, false otherwise.
	 */
	public function validate_cancel_payment_request( WP_REST_Request $request ) {
		if ( $request->get_header( 'Nonce' ) ) {
			$nonce = $request->get_header( 'Nonce' );
			return wp_verify_nonce( $nonce, 'wc_gateway_paypal_standard_cancel_payment' );
		}
		return false;
	}

	/**
	 * Create a PayPal order.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response The response object.
	 */
	public function create_order( WP_REST_Request $request ) {
		$data = $request->get_json_params();

		if ( empty( $data['order_id'] ) || empty( $data['order_key'] ) ) {
			return new WP_REST_Response( array( 'error' => 'Invalid request' ), 400 );
		}

		$payment_source = isset( $data['payment_source'] ) ? sanitize_text_field( $data['payment_source'] ) : '';
		if ( empty( $payment_source ) || ! in_array( $payment_source, WC_Gateway_Paypal_Constants::SUPPORTED_PAYMENT_SOURCES, true ) ) {
			return new WP_REST_Response( array( 'error' => 'Missing/Invalid payment source: ' . esc_html( $payment_source ) ), 400 );
		}

		$order_id = $data['order_id'];
		$order    = wc_get_order( $order_id );

		if ( ! $order ) {
			return new WP_REST_Response( array( 'error' => 'Order not found' ), 404 );
		}

		$order_key = $data['order_key'];
		if ( ! $order_key || ! hash_equals( $order->get_order_key(), $order_key ) ) {
			return new WP_REST_Response( array( 'error' => 'Order not found' ), 404 );
		}

		if ( ! in_array( $order->get_status(), array( OrderStatus::CHECKOUT_DRAFT, OrderStatus::PENDING ), true ) ) {
			return new WP_REST_Response( array( 'error' => 'Invalid order status' ), 409 );
		}

		$gateway = WC_Gateway_Paypal::get_instance();

		// For Buttons requests, we need to explicitly set the payment method to PayPal.
		$order->set_payment_method( $gateway->id );
		$order->save();

		$paypal_request = new WC_Gateway_Paypal_Request( $gateway );
		$paypal_order   = $paypal_request->create_paypal_order(
			$order,
			$payment_source,
			array(
				'is_js_sdk_flow'            => true,
				'app_switch_request_origin' => $data['app_switch_request_origin'] ?? '',
			)
		);

		if ( ! $paypal_order || empty( $paypal_order['id'] ) ) {
			return new WP_REST_Response( array( 'error' => 'Failed to create PayPal order' ), 400 );
		}

		$order->update_meta_data( '_paypal_order_id', $paypal_order['id'] );
		$order->update_status( OrderStatus::PENDING );
		$order->save();

		return new WP_REST_Response(
			array(
				'paypal_order_id' => $paypal_order['id'] ?? null,
				'order_id'        => $order_id,
				'return_url'      => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $gateway->get_return_url( $order ) ) ),
			),
			200
		);
	}

	/**
	 * Cancel a PayPal payment. This is used to move the woocommerce order back to a draft status.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response The response object.
	 */
	public function cancel_payment( WP_REST_Request $request ) {
		$data = $request->get_json_params();

		$order_id        = isset( $data['order_id'] ) ? absint( $data['order_id'] ) : 0;
		$paypal_order_id = isset( $data['paypal_order_id'] ) ? wc_clean( $data['paypal_order_id'] ) : '';
		if ( ! $order_id || '' === $paypal_order_id ) {
			return new WP_REST_Response( array( 'error' => 'Invalid request' ), 400 );
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return new WP_REST_Response( array( 'error' => 'Order not found' ), 404 );
		}

		// Verify order by checking the PayPal order ID.
		$paypal_order_id_from_meta = $order->get_meta( '_paypal_order_id' );
		if ( $paypal_order_id_from_meta !== $paypal_order_id ) {
			return new WP_REST_Response( array( 'error' => 'Invalid PayPal order' ), 404 );
		}

		// If order is already in draft status, do nothing and return success.
		if ( $order->has_status( OrderStatus::CHECKOUT_DRAFT ) ) {
			return new WP_REST_Response( array( 'success' => true ), 200 );
		}

		// If order is not pending, return an error.
		if ( ! $order->has_status( OrderStatus::PENDING ) ) {
			return new WP_REST_Response( array( 'error' => 'Order is not pending' ), 409 );
		}

		$order->update_status( OrderStatus::CHECKOUT_DRAFT );
		$order->save();

		return new WP_REST_Response( array( 'success' => true ), 200 );
	}
}