diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index c8c62e1b6..588a89228 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\AddressFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory; @@ -43,7 +44,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; -use WpOop\TransientCache\CachePoolFactory; return array( 'api.host' => function( $container ) : string { @@ -214,6 +214,11 @@ return array( 'api.factory.webhook' => static function ( $container ): WebhookFactory { return new WebhookFactory(); }, + 'api.factory.capture' => static function ( $container ): CaptureFactory { + + $amount_factory = $container->get( 'api.factory.amount' ); + return new CaptureFactory( $amount_factory ); + }, 'api.factory.purchase-unit' => static function ( $container ): PurchaseUnitFactory { $amount_factory = $container->get( 'api.factory.amount' ); @@ -277,7 +282,8 @@ return array( }, 'api.factory.payments' => static function ( $container ): PaymentsFactory { $authorizations_factory = $container->get( 'api.factory.authorization' ); - return new PaymentsFactory( $authorizations_factory ); + $capture_factory = $container->get( 'api.factory.capture' ); + return new PaymentsFactory( $authorizations_factory, $capture_factory ); }, 'api.factory.authorization' => static function ( $container ): AuthorizationFactory { return new AuthorizationFactory(); diff --git a/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php index 7ee7daaa3..4011668c1 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; @@ -188,4 +189,63 @@ class PaymentsEndpoint { $authorization = $this->authorizations_factory->from_paypal_response( $json ); return $authorization; } + + /** + * Refunds a payment. + * + * @param Refund $refund The refund to be processed. + * + * @return bool + * @throws RuntimeException If the request fails. + */ + public function refund( Refund $refund ) : bool { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $refund->to_array() ), + ); + + $response = $this->request( $url, $args ); + $json = json_decode( $response['body'] ); + + if ( is_wp_error( $response ) ) { + $error = new RuntimeException( + __( 'Could not refund payment.', 'paypal-payments-for-woocommerce' ) + ); + $this->logger->log( + 'warning', + $error->getMessage(), + array( + 'args' => $args, + 'response' => $response, + ) + ); + throw $error; + } + + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 201 !== $status_code ) { + $error = new PayPalApiException( + $json, + $status_code + ); + $this->logger->log( + 'warning', + $error->getMessage(), + array( + 'args' => $args, + 'response' => $response, + ) + ); + throw $error; + } + + return true; + } } diff --git a/modules/ppcp-api-client/src/Entity/class-capture.php b/modules/ppcp-api-client/src/Entity/class-capture.php new file mode 100644 index 000000000..9aad9e029 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-capture.php @@ -0,0 +1,197 @@ +id = $id; + $this->status = $status; + $this->status_details = $status_details; + $this->amount = $amount; + $this->final_capture = $final_capture; + $this->seller_protection = $seller_protection; + $this->invoice_id = $invoice_id; + $this->custom_id = $custom_id; + } + + /** + * Returns the ID. + * + * @return string + */ + public function id() : string { + return $this->id; + } + + /** + * Returns the status. + * + * @return string + */ + public function status() : string { + return $this->status; + } + + /** + * Returns the status details object. + * + * @return \stdClass + */ + public function status_details() : \stdClass { + return (object) array( 'reason' => $this->status_details ); + } + + /** + * Returns the amount. + * + * @return Amount + */ + public function amount() : Amount { + return $this->amount; + } + + /** + * Returns whether this is the final capture or not. + * + * @return bool + */ + public function final_capture() : bool { + return $this->final_capture; + } + + /** + * Returns the seller protection object. + * + * @return \stdClass + */ + public function seller_protection() : \stdClass { + return (object) array( 'status' => $this->seller_protection ); + } + + /** + * Returns the invoice id. + * + * @return string + */ + public function invoice_id() : string { + return $this->invoice_id; + } + + /** + * Returns the custom id. + * + * @return string + */ + public function custom_id() : string { + return $this->custom_id; + } + + /** + * Returns the entity as array. + * + * @return array + */ + public function to_array() : array { + return array( + 'id' => $this->id(), + 'status' => $this->status(), + 'status_details' => (array) $this->status_details(), + 'amount' => $this->amount()->to_array(), + 'final_capture' => $this->final_capture(), + 'seller_protection' => (array) $this->seller_protection(), + 'invoice_id' => $this->invoice_id(), + 'custom_id' => $this->custom_id(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/class-payments.php b/modules/ppcp-api-client/src/Entity/class-payments.php index 75febd6b6..d5e051f88 100644 --- a/modules/ppcp-api-client/src/Entity/class-payments.php +++ b/modules/ppcp-api-client/src/Entity/class-payments.php @@ -21,13 +21,34 @@ class Payments { */ private $authorizations; + /** + * The Captures. + * + * @var Capture[] + */ + private $captures; + /** * Payments constructor. * - * @param Authorization ...$authorizations The Authorizations. + * @param array $authorizations The Authorizations. + * @param array $captures The Captures. */ - public function __construct( Authorization ...$authorizations ) { + public function __construct( array $authorizations, array $captures ) { + foreach ( $authorizations as $key => $authorization ) { + if ( is_a( $authorization, Authorization::class ) ) { + continue; + } + unset( $authorizations[ $key ] ); + } + foreach ( $captures as $key => $capture ) { + if ( is_a( $capture, Capture::class ) ) { + continue; + } + unset( $captures[ $key ] ); + } $this->authorizations = $authorizations; + $this->captures = $captures; } /** @@ -43,6 +64,12 @@ class Payments { }, $this->authorizations() ), + 'captures' => array_map( + static function ( Capture $capture ): array { + return $capture->to_array(); + }, + $this->captures() + ), ); } @@ -54,4 +81,13 @@ class Payments { public function authorizations(): array { return $this->authorizations; } + + /** + * Returns the Captures. + * + * @return Capture[] + **/ + public function captures(): array { + return $this->captures; + } } diff --git a/modules/ppcp-api-client/src/Entity/class-refund.php b/modules/ppcp-api-client/src/Entity/class-refund.php new file mode 100644 index 000000000..71c2a2f56 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-refund.php @@ -0,0 +1,118 @@ +capture = $capture; + $this->invoice_id = $invoice_id; + $this->note_to_payer = $note_to_payer; + $this->amount = $amount; + } + + /** + * Returns the capture for the refund. + * + * @return Capture + */ + public function for_capture() : Capture { + return $this->capture; + } + + /** + * Return the invoice id. + * + * @return string + */ + public function invoice_id() : string { + return $this->invoice_id; + } + + /** + * Returns the note to the payer. + * + * @return string + */ + public function note_to_payer() : string { + return $this->note_to_payer; + } + + /** + * Returns the Amount. + * + * @return Amount|null + */ + public function amount() { + return $this->amount; + } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array() : array { + $data = array( + 'invoice_id' => $this->invoice_id(), + ); + if ( $this->note_to_payer() ) { + $data['note_to_payer'] = $this->note_to_payer(); + } + if ( $this->amount() ) { + $data['amount'] = $this->amount()->to_array(); + } + return $data; + } +} diff --git a/modules/ppcp-api-client/src/Factory/class-capturefactory.php b/modules/ppcp-api-client/src/Factory/class-capturefactory.php new file mode 100644 index 000000000..7389f67e5 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/class-capturefactory.php @@ -0,0 +1,57 @@ +amount_factory = $amount_factory; + } + + /** + * Returns the capture object based off the PayPal response. + * + * @param \stdClass $data The PayPal response. + * + * @return Capture + */ + public function from_paypal_response( \stdClass $data ) : Capture { + + $reason = isset( $data->status_details->reason ) ? (string) $data->status_details->reason : ''; + return new Capture( + (string) $data->id, + (string) $data->status, + $reason, + $this->amount_factory->from_paypal_response( $data->amount ), + (bool) $data->final_capture, + (string) $data->seller_protection->status, + (string) $data->invoice_id, + (string) $data->custom_id + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/class-payerfactory.php b/modules/ppcp-api-client/src/Factory/class-payerfactory.php index 3f9ee2755..9b9c57a8f 100644 --- a/modules/ppcp-api-client/src/Factory/class-payerfactory.php +++ b/modules/ppcp-api-client/src/Factory/class-payerfactory.php @@ -36,6 +36,42 @@ class PayerFactory { $this->address_factory = $address_factory; } + /** + * Returns a Payer entity from a WooCommerce order. + * + * @param \WC_Order $wc_order The WooCommerce order. + * + * @return Payer + */ + public function from_wc_order( \WC_Order $wc_order ): Payer { + $payer_id = ''; + $birthdate = null; + + $phone = null; + if ( $wc_order->get_billing_phone() ) { + // make sure the phone number contains only numbers and is max 14. chars long. + $national_number = $wc_order->get_billing_phone(); + $national_number = preg_replace( '/[^0-9]/', '', $national_number ); + $national_number = substr( $national_number, 0, 14 ); + + $phone = new PhoneWithType( + 'HOME', + new Phone( $national_number ) + ); + } + return new Payer( + new PayerName( + $wc_order->get_billing_first_name(), + $wc_order->get_billing_last_name() + ), + $wc_order->get_billing_email(), + $payer_id, + $this->address_factory->from_wc_order( $wc_order, 'billing' ), + $birthdate, + $phone + ); + } + /** * Returns a Payer object based off a WooCommerce customer. * diff --git a/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php b/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php index 241fc57eb..353c71f44 100644 --- a/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php +++ b/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; /** @@ -24,16 +25,26 @@ class PaymentsFactory { */ private $authorization_factory; + /** + * The Capture factory. + * + * @var CaptureFactory + */ + private $capture_factory; + /** * PaymentsFactory constructor. * - * @param AuthorizationFactory $authorization_factory The AuthorizationFactory. + * @param AuthorizationFactory $authorization_factory The Authorization factory. + * @param CaptureFactory $capture_factory The Capture factory. */ public function __construct( - AuthorizationFactory $authorization_factory + AuthorizationFactory $authorization_factory, + CaptureFactory $capture_factory ) { $this->authorization_factory = $authorization_factory; + $this->capture_factory = $capture_factory; } /** @@ -50,7 +61,13 @@ class PaymentsFactory { }, isset( $data->authorizations ) ? $data->authorizations : array() ); - $payments = new Payments( ...$authorizations ); + $captures = array_map( + function ( \stdClass $authorization ): Capture { + return $this->capture_factory->from_paypal_response( $authorization ); + }, + isset( $data->captures ) ? $data->captures : array() + ); + $payments = new Payments( $authorizations, $captures ); return $payments; } } diff --git a/modules/ppcp-api-client/src/Helper/class-dccapplies.php b/modules/ppcp-api-client/src/Helper/class-dccapplies.php index ddc259646..2984be4a4 100644 --- a/modules/ppcp-api-client/src/Helper/class-dccapplies.php +++ b/modules/ppcp-api-client/src/Helper/class-dccapplies.php @@ -38,115 +38,7 @@ class DccApplies { 'SGD', 'USD', ), - 'BE' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'BG' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'CY' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'CZ' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'DK' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'EE' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'FI' => array( + 'ES' => array( 'AUD', 'CAD', 'CHF', @@ -182,25 +74,7 @@ class DccApplies { 'SGD', 'USD', ), - 'GR' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'HU' => array( + 'GB' => array( 'AUD', 'CAD', 'CHF', @@ -236,258 +110,6 @@ class DccApplies { 'SGD', 'USD', ), - 'LV' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'LI' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'LT' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'LU' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'MT' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'NL' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'NO' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'PL' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'PT' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'RO' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'SK' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'SI' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'ES' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), - 'SE' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), 'US' => array( 'AUD', 'CAD', @@ -496,25 +118,45 @@ class DccApplies { 'JPY', 'USD', ), - 'GB' => array( - 'AUD', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'JPY', - 'NOK', - 'NZD', - 'PLN', - 'SEK', - 'SGD', - 'USD', - ), + ); + /** + * Which countries support which credit cards. Empty credit card arrays mean no restriction on + * currency. Otherwise only the currencies in the array are supported. + * + * @var array + */ + private $country_card_matrix = array( + 'AU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'ES' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'FR' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'GB' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'GBP', 'USD' ), + ), + 'IT' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'US' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'USD' ), + 'discover' => array( 'USD' ), + ), ); /** @@ -523,8 +165,7 @@ class DccApplies { * @return bool */ public function for_country_currency(): bool { - $region = wc_get_base_location(); - $country = $region['country']; + $country = $this->country(); $currency = get_woocommerce_currency(); if ( ! in_array( $country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { return false; @@ -532,4 +173,67 @@ class DccApplies { $applies = in_array( $currency, $this->allowed_country_currency_matrix[ $country ], true ); return $applies; } + + /** + * Returns credit cards, which can be used. + * + * @return array + */ + public function valid_cards() : array { + $country = $this->country(); + $cards = array(); + if ( ! isset( $this->country_card_matrix[ $country ] ) ) { + return $cards; + } + + $supported_currencies = $this->country_card_matrix[ $country ]; + foreach ( $supported_currencies as $card => $currencies ) { + if ( $this->can_process_card( $card ) ) { + $cards[] = $card; + } + } + if ( in_array( 'amex', $cards, true ) ) { + $cards[] = 'american-express'; + } + if ( in_array( 'mastercard', $cards, true ) ) { + $cards[] = 'master-card'; + } + return $cards; + } + + /** + * Whether a card can be used or not. + * + * @param string $card The card. + * + * @return bool + */ + public function can_process_card( string $card ) : bool { + $country = $this->country(); + if ( ! isset( $this->country_card_matrix[ $country ] ) ) { + return false; + } + if ( ! isset( $this->country_card_matrix[ $country ][ $card ] ) ) { + return false; + } + + /** + * If the supported currencies array is empty, there are no + * restrictions, which currencies are supported by a card. + */ + $supported_currencies = $this->country_card_matrix[ $country ][ $card ]; + $currency = get_woocommerce_currency(); + return empty( $supported_currencies ) || in_array( $currency, $supported_currencies, true ); + } + + /** + * Returns the country code of the shop. + * + * @return string + */ + private function country() : string { + $region = wc_get_base_location(); + $country = $region['country']; + return $country; + } } diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index f52b8127d..881e9f9b6 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -2,15 +2,18 @@ import MiniCartBootstap from './modules/ContextBootstrap/MiniCartBootstap'; import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap'; import CartBootstrap from './modules/ContextBootstrap/CartBootstap'; import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap'; +import PayNowBootstrap from "./modules/ContextBootstrap/PayNowBootstrap"; import Renderer from './modules/Renderer/Renderer'; import ErrorHandler from './modules/ErrorHandler'; import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler"; import MessageRenderer from "./modules/Renderer/MessageRenderer"; +import Spinner from "./modules/Helper/Spinner"; const bootstrap = () => { const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic); - const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler); + const spinner = new Spinner(); + const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner); const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const context = PayPalCommerceGateway.context; @@ -46,12 +49,22 @@ const bootstrap = () => { const checkoutBootstap = new CheckoutBootstap( PayPalCommerceGateway, renderer, - messageRenderer + messageRenderer, + spinner ); checkoutBootstap.init(); } + if (context === 'pay-now' ) { + const payNowBootstrap = new PayNowBootstrap( + PayPalCommerceGateway, + renderer, + messageRenderer + ); + payNowBootstrap.init(); + } + if (context !== 'checkout') { messageRenderer.render(); } diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index bd83ab1af..655d5dedc 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -3,12 +3,14 @@ import {payerData} from "../Helper/PayerData"; class CheckoutActionHandler { - constructor(config, errorHandler) { + constructor(config, errorHandler, spinner) { this.config = config; this.errorHandler = errorHandler; + this.spinner = spinner; } configuration() { + const spinner = this.spinner; const createOrder = (data, actions) => { const payer = payerData(); const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ? @@ -16,7 +18,8 @@ class CheckoutActionHandler { const errorHandler = this.errorHandler; - const formValues = jQuery('form.checkout').serialize(); + const formSelector = this.config.context === 'checkout' ? 'form.checkout' : 'form#order_review'; + const formValues = jQuery(formSelector).serialize(); return fetch(this.config.ajax.create_order.endpoint, { method: 'POST', @@ -25,12 +28,14 @@ class CheckoutActionHandler { payer, bn_code:bnCode, context:this.config.context, + order_id:this.config.order_id, form:formValues }) }).then(function (res) { return res.json(); }).then(function (data) { if (!data.success) { + spinner.unblock(); errorHandler.message(data.data.message, true); return; } @@ -38,13 +43,13 @@ class CheckoutActionHandler { input.setAttribute('type', 'hidden'); input.setAttribute('name', 'ppcp-resume-order'); input.setAttribute('value', data.data.purchase_units[0].custom_id); - document.querySelector('form.checkout').append(input); + document.querySelector(formSelector).append(input); return data.data.id; }); } return { createOrder, - onApprove:onApprove(this, this.errorHandler), + onApprove:onApprove(this, this.errorHandler, this.spinner), onError: (error) => { this.errorHandler.genericError(); } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index de46e5759..85dfbcdc5 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -2,10 +2,11 @@ import ErrorHandler from '../ErrorHandler'; import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; class CheckoutBootstap { - constructor(gateway, renderer, messages) { + constructor(gateway, renderer, messages, spinner) { this.gateway = gateway; this.renderer = renderer; - this.messages = messages + this.messages = messages; + this.spinner = spinner; } init() { @@ -41,6 +42,7 @@ class CheckoutBootstap { const actionHandler = new CheckoutActionHandler( PayPalCommerceGateway, new ErrorHandler(this.gateway.labels.error.generic), + this.spinner ); this.renderer.render( diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js new file mode 100644 index 000000000..b6b147802 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js @@ -0,0 +1,80 @@ +import ErrorHandler from '../ErrorHandler'; +import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; + +class PayNowBootstrap { + constructor(gateway, renderer, messages) { + this.gateway = gateway; + this.renderer = renderer; + this.messages = messages + } + + init() { + + this.render(); + + jQuery(document.body).on('updated_checkout', () => { + this.render(); + }); + + jQuery(document.body). + on('updated_checkout payment_method_selected', () => { + this.switchBetweenPayPalandOrderButton(); + }); + this.switchBetweenPayPalandOrderButton(); + } + + shouldRender() { + if (document.querySelector(this.gateway.button.cancel_wrapper)) { + return false; + } + + return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null; + } + + render() { + if (!this.shouldRender()) { + return; + } + if (document.querySelector(this.gateway.hosted_fields.wrapper + '>div')) { + document.querySelector(this.gateway.hosted_fields.wrapper + '>div').setAttribute('style', ''); + } + const actionHandler = new CheckoutActionHandler( + PayPalCommerceGateway, + new ErrorHandler(this.gateway.labels.error.generic), + ); + + this.renderer.render( + this.gateway.button.wrapper, + this.gateway.hosted_fields.wrapper, + actionHandler.configuration(), + ); + } + + switchBetweenPayPalandOrderButton() { + const currentPaymentMethod = jQuery( + 'input[name="payment_method"]:checked').val(); + + if (currentPaymentMethod !== 'ppcp-gateway' && currentPaymentMethod !== 'ppcp-credit-card-gateway') { + this.renderer.hideButtons(this.gateway.button.wrapper); + this.renderer.hideButtons(this.gateway.messages.wrapper); + this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); + jQuery('#place_order').show(); + } + else { + jQuery('#place_order').hide(); + if (currentPaymentMethod === 'ppcp-gateway') { + this.renderer.showButtons(this.gateway.button.wrapper); + this.renderer.showButtons(this.gateway.messages.wrapper); + this.messages.render(); + this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); + } + if (currentPaymentMethod === 'ppcp-credit-card-gateway') { + this.renderer.hideButtons(this.gateway.button.wrapper); + this.renderer.hideButtons(this.gateway.messages.wrapper); + this.renderer.showButtons(this.gateway.hosted_fields.wrapper); + } + } + } +} + +export default PayNowBootstrap; \ No newline at end of file diff --git a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js new file mode 100644 index 000000000..50e4eebd9 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js @@ -0,0 +1,23 @@ +class Spinner { + + constructor() { + this.target = 'form.woocommerce-checkout'; + } + block() { + + jQuery( this.target ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + } + + unblock() { + + jQuery( this.target ).unblock(); + } +} + +export default Spinner; \ No newline at end of file diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js index 61e9bcb9a..e38434853 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js @@ -1,5 +1,6 @@ -const onApprove = (context, errorHandler) => { +const onApprove = (context, errorHandler, spinner) => { return (data, actions) => { + spinner.block(); return fetch(context.config.ajax.approve_order.endpoint, { method: 'POST', body: JSON.stringify({ @@ -9,10 +10,14 @@ const onApprove = (context, errorHandler) => { }).then((res)=>{ return res.json(); }).then((data)=>{ + spinner.unblock(); if (!data.success) { - errorHandler.genericError(); - console.error(data); - if (typeof actions.restart !== 'undefined') { + if (data.data.code === 100) { + errorHandler.message(data.data.message); + } else { + errorHandler.genericError(); + } + if (typeof actions !== 'undefined' && typeof actions.restart !== 'undefined') { return actions.restart(); } throw new Error(data.data.message); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index ec38441af..48327ddba 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -2,15 +2,20 @@ import dccInputFactory from "../Helper/DccInputFactory"; class CreditCardRenderer { - constructor(defaultConfig, errorHandler) { + constructor(defaultConfig, errorHandler, spinner) { this.defaultConfig = defaultConfig; this.errorHandler = errorHandler; + this.spinner = spinner; + this.cardValid = false; } render(wrapper, contextConfig) { if ( - this.defaultConfig.context !== 'checkout' + ( + this.defaultConfig.context !== 'checkout' + && this.defaultConfig.context !== 'pay-now' + ) || wrapper === null || document.querySelector(wrapper) === null ) { @@ -87,6 +92,7 @@ class CreditCardRenderer { } }).then(hostedFields => { const submitEvent = (event) => { + this.spinner.block(); if (event) { event.preventDefault(); } @@ -96,7 +102,7 @@ class CreditCardRenderer { return state.fields[key].isValid; }); - if (formValid) { + if (formValid && this.cardValid) { let vault = document.querySelector(wrapper + ' .ppcp-credit-card-vault') ? document.querySelector(wrapper + ' .ppcp-credit-card-vault').checked : false; @@ -107,15 +113,26 @@ class CreditCardRenderer { vault }).then((payload) => { payload.orderID = payload.orderId; + this.spinner.unblock(); return contextConfig.onApprove(payload); }); } else { - this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); + this.spinner.unblock(); + const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; + this.errorHandler.message(message); } } hostedFields.on('inputSubmitRequest', function () { submitEvent(null); }); + hostedFields.on('cardTypeChange', (event) => { + if ( ! event.cards.length ) { + this.cardValid = false; + return; + } + const validCards = this.defaultConfig.hosted_fields.valid_cards; + this.cardValid = validCards.indexOf(event.cards[0].type) !== -1; + }) document.querySelector(wrapper + ' button').addEventListener( 'click', submitEvent diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index a4d288577..40e9954d9 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; -use Dhii\Data\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; @@ -107,16 +106,18 @@ return array( return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store ); }, 'button.endpoint.create-order' => static function ( $container ): CreateOrderEndpoint { - $request_data = $container->get( 'button.request-data' ); - $repository = $container->get( 'api.repository.cart' ); - $order_endpoint = $container->get( 'api.endpoint.order' ); - $payer_factory = $container->get( 'api.factory.payer' ); - $session_handler = $container->get( 'session.handler' ); - $settings = $container->get( 'wcgateway.settings' ); - $early_order_handler = $container->get( 'button.helper.early-order-handler' ); + $request_data = $container->get( 'button.request-data' ); + $cart_repository = $container->get( 'api.repository.cart' ); + $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); + $order_endpoint = $container->get( 'api.endpoint.order' ); + $payer_factory = $container->get( 'api.factory.payer' ); + $session_handler = $container->get( 'session.handler' ); + $settings = $container->get( 'wcgateway.settings' ); + $early_order_handler = $container->get( 'button.helper.early-order-handler' ); return new CreateOrderEndpoint( $request_data, - $repository, + $cart_repository, + $purchase_unit_factory, $order_endpoint, $payer_factory, $session_handler, @@ -134,10 +135,19 @@ return array( }, 'button.endpoint.approve-order' => static function ( $container ): ApproveOrderEndpoint { $request_data = $container->get( 'button.request-data' ); - $order_endpoint = $container->get( 'api.endpoint.order' ); + $order_endpoint = $container->get( 'api.endpoint.order' ); $session_handler = $container->get( 'session.handler' ); - $three_d_secure = $container->get( 'button.helper.three-d-secure' ); - return new ApproveOrderEndpoint( $request_data, $order_endpoint, $session_handler, $three_d_secure ); + $three_d_secure = $container->get( 'button.helper.three-d-secure' ); + $settings = $container->get( 'wcgateway.settings' ); + $dcc_applies = $container->get( 'api.helpers.dccapplies' ); + return new ApproveOrderEndpoint( + $request_data, + $order_endpoint, + $session_handler, + $three_d_secure, + $settings, + $dcc_applies + ); }, 'button.endpoint.data-client-id' => static function( $container ) : DataClientIdEndpoint { $request_data = $container->get( 'button.request-data' ); diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 017a7acfd..8cb631271 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -177,6 +177,15 @@ class SmartButton implements SmartButtonInterface { ), 11 ); + + add_action( + 'woocommerce_pay_order_after_submit', + array( + $this, + 'dcc_renderer', + ), + 11 + ); } return true; } @@ -232,6 +241,14 @@ class SmartButton implements SmartButtonInterface { ), 11 ); + add_action( + 'woocommerce_pay_order_after_submit', + array( + $this, + 'message_renderer', + ), + 11 + ); } return true; } @@ -294,6 +311,7 @@ class SmartButton implements SmartButtonInterface { } add_action( 'woocommerce_review_order_after_submit', array( $this, 'button_renderer' ), 10 ); + add_action( 'woocommerce_pay_order_after_submit', array( $this, 'button_renderer' ), 10 ); return true; } @@ -321,7 +339,7 @@ class SmartButton implements SmartButtonInterface { $load_script = true; } - if ( is_checkout() && $this->can_render_dcc() ) { + if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) && $this->can_render_dcc() ) { wp_enqueue_style( 'ppcp-hosted-fields', $this->module_url . '/assets/css/hosted-fields.css', @@ -474,7 +492,8 @@ class SmartButton implements SmartButtonInterface { */ private function can_render_dcc() : bool { - return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency(); + $can_render = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency(); + return $can_render; } /** @@ -502,6 +521,8 @@ class SmartButton implements SmartButtonInterface { esc_html__( 'Save your card', 'paypal-payments-for-woocommerce' ) ) : ''; + $label = 'checkout' === $this->context() ? __( 'Place order', 'paypal-payments-for-woocommerce' ) : __( 'Pay for order', 'paypal-payments-for-woocommerce' ); + printf( '