diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index 13b914335..8f8edd3c9 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -14,6 +14,7 @@ const initiateRedirect = ( successUrl ) => { }; const onApprove = ( context, errorHandler ) => { + console.log( 'onApprove' ); return ( data, actions ) => { const canCreateOrder = ! context.config.vaultingEnabled || data.paymentSource !== 'venmo'; @@ -48,7 +49,6 @@ const onApprove = ( context, errorHandler ) => { } ); } - const orderReceivedUrl = approveData.data?.order_received_url; initiateRedirect( orderReceivedUrl || context.config.redirect ); } ); }; diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 44e9a52ac..952e308c3 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -10,7 +10,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; @@ -282,6 +284,19 @@ return array( $container->get( 'wcgateway.paypal-gateway' ) ); }, + 'button.endpoint.get-order' => static function( ContainerInterface $container ): GetOrderEndpoint { + return new GetOrderEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.order' ) + ); + }, + 'button.endpoint.capture-order' => static function( ContainerInterface $container ): CaptureOrderEndpoint { + return new CaptureOrderEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.order' ), + $container->get( 'button.helper.wc-order-creator' ), + ); + }, 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { return new CheckoutFormSaver( $container->get( 'session.handler' ) diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 81ced3a68..ff33158ad 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -24,10 +24,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; @@ -1143,10 +1145,18 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), + 'get_order' => array( + 'endpoint' => \WC_AJAX::get_endpoint( GetOrderEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( GetOrderEndpoint::nonce() ), + ), 'create_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), ), + 'capture_order' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CaptureOrderEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( CaptureOrderEndpoint::nonce() ), + ), 'approve_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), diff --git a/modules/ppcp-button/src/ButtonModule.php b/modules/ppcp-button/src/ButtonModule.php index cd2c8d381..160943f28 100644 --- a/modules/ppcp-button/src/ButtonModule.php +++ b/modules/ppcp-button/src/ButtonModule.php @@ -10,7 +10,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; @@ -177,6 +179,26 @@ class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); + add_action( + 'wc_ajax_' . GetOrderEndpoint::ENDPOINT, + static function () use ( $container ) { + $endpoint = $container->get( 'button.endpoint.get-order' ); + assert( $endpoint instanceof GetOrderEndpoint ); + + $endpoint->handle_request(); + } + ); + + add_action( + 'wc_ajax_' . CaptureOrderEndpoint::ENDPOINT, + static function () use ( $container ) { + $endpoint = $container->get( 'button.endpoint.capture-order' ); + assert( $endpoint instanceof GetOrderEndpoint ); + + $endpoint->handle_request(); + } + ); + add_action( 'wc_ajax_' . CreateOrderEndpoint::ENDPOINT, static function () use ( $container ) { diff --git a/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php new file mode 100644 index 000000000..9379f9521 --- /dev/null +++ b/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php @@ -0,0 +1,98 @@ +request_data = $request_data; + $this->order_endpoint = $order_endpoint; + $this->wc_order_creator = $wc_order_creator; + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws RuntimeException When order not found or handling failed. + */ + public function handle_request(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + if ( ! isset( $data['order_id'] ) ) { + throw new RuntimeException( + __( 'No order id given', 'woocommerce-paypal-payments' ) + ); + } + $order = $this->order_endpoint->order( $data['order_id'] ); + $order = $this->order_endpoint->capture( $order ); + if ($order->status()->is('COMPLETED')) { + $wc_order = $this->wc_order_creator->create_from_paypal_order( $order, WC()->cart ); + $order_received_url = $wc_order->get_checkout_order_received_url(); + + wp_send_json_success( array( 'order_received_url' => $order_received_url ) ); + } + + wp_send_json_success(); + return true; + } +} diff --git a/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php new file mode 100644 index 000000000..6ce974e3e --- /dev/null +++ b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php @@ -0,0 +1,84 @@ +request_data = $request_data; + $this->order_endpoint = $order_endpoint; + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws RuntimeException When order not found or handling failed. + */ + public function handle_request(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + if ( ! isset( $data['order_id'] ) ) { + throw new RuntimeException( + __( 'No order id given', 'woocommerce-paypal-payments' ) + ); + } + + $order = $this->order_endpoint->order( $data['order_id'] ); + + wp_send_json_success(array( + 'order' => $order + )); + return true; + } +} diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index d49bee615..262b6f098 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -55,6 +55,53 @@ class BaseHandler { return this.actionHandler().configuration().onApprove( data, actions ); } + captureOrder( data, actions ) { + return fetch( this.ppcpConfig.ajax.get_order.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify( { + nonce: this.ppcpConfig.ajax.get_order.nonce, + order_id: data.orderID, + } ), + } ) + .then( ( order ) => { + console.log( 'order', order ); + const orderResponse = order.json(); + console.log( + orderResponse?.payment_source?.google_pay?.card + ?.authentication_result + ); + + return fetch( this.ppcpConfig.ajax.capture_order.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify( { + nonce: this.ppcpConfig.ajax.capture_order.nonce, + order_id: data.orderID, + } ), + } ); + } ) + .then( ( response ) => response.json() ) + .then( ( captureResponse ) => { + console.log( 'Capture response:', captureResponse ); + const orderReceivedUrl = + captureResponse.data?.order_received_url; + console.log( 'orderReceivedUrl', orderReceivedUrl ); + setTimeout( () => { + window.location.href = orderReceivedUrl; + }, 200 ); + } ) + .catch( ( error ) => { + console.error( 'Error:', error ); + } ); + } + actionHandler() { return new CartActionHandler( this.ppcpConfig, this.errorHandler() ); } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index c82a3a068..f24274f0a 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -193,6 +193,7 @@ class GooglepayButton extends PaymentButton { this.onButtonClick = this.onButtonClick.bind( this ); this.log( 'Create instance' ); + this.log( this.ppcpConfig ); } /** @@ -847,7 +848,31 @@ class GooglepayButton extends PaymentButton { this.log( 'confirmOrder', confirmOrderResponse ); - return 'APPROVED' === confirmOrderResponse?.status; + switch ( confirmOrderResponse?.status ) { + case 'APPROVED': + return true; + case 'PAYER_ACTION_REQUIRED': + return 'action_required'; + default: + return false; + } + }; + /** + * Initiates payer action and handles the 3DS contingency. + * + * @param {string} orderID + */ + const initiatePayerAction = async ( orderID ) => { + this.log( 'initiatePayerAction', orderID ); + + this.log( + '==== Confirm Payment Completed Payer Action Required =====' + ); + await widgetBuilder.paypal + .Googlepay() + .initiatePayerAction( { orderId: orderID } ); + + this.log( '===== Payer Action Completed =====' ); }; /** @@ -887,6 +912,28 @@ class GooglepayButton extends PaymentButton { return isApproved; }; + const captureOrderServerSide = async ( orderID ) => { + let isCaptured = true; + this.log( 'context', this.contextHandler ); + await this.contextHandler.captureOrder( + { orderID, payer }, + { + restart: () => + new Promise( ( resolve ) => { + isCaptured = false; + resolve(); + } ), + order: { + get: () => + new Promise( ( resolve ) => { + resolve( null ); + } ), + }, + } + ); + return isCaptured; + }; + // Add billing data to session. moduleStorage.setPayer( payer ); setPayerData( payer ); @@ -899,6 +946,14 @@ class GooglepayButton extends PaymentButton { if ( ! isApprovedByPayPal ) { result = paymentError( 'TRANSACTION FAILED' ); + } else if ( isApprovedByPayPal === 'action_required' ) { + await initiatePayerAction( orderId ); + const success = await captureOrderServerSide( orderId ); + if ( success ) { + result = paymentResponse( 'SUCCESS' ); + } else { + result = paymentError( 'FAILED TO APPROVE' ); + } } else { const success = await approveOrderServerSide( orderId );