From fd7dfaf13b26d1a84d1f3b79e985dfd19c5428b3 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Thu, 25 Mar 2021 16:11:45 +0100 Subject: [PATCH] Allow credit card gateway to use saved credit cards --- .../src/Entity/class-paymenttoken.php | 23 +++++++- .../src/Factory/class-paymenttokenfactory.php | 4 +- .../ContextBootstrap/CheckoutBootstap.js | 16 ++++- modules/ppcp-button/services.php | 4 +- .../src/Assets/class-smartbutton.php | 58 +++++++++++++++++-- .../class-paymenttokenrepository.php | 20 +++++++ .../src/class-subscriptionmodule.php | 18 ++++++ modules/ppcp-wc-gateway/services.php | 13 ++++- .../src/Gateway/class-creditcardgateway.php | 38 +++++++++++- .../src/Gateway/class-processpaymenttrait.php | 51 ++++++++++++++++ 10 files changed, 229 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/class-paymenttoken.php b/modules/ppcp-api-client/src/Entity/class-paymenttoken.php index 555dde2d9..e8ea6c789 100644 --- a/modules/ppcp-api-client/src/Entity/class-paymenttoken.php +++ b/modules/ppcp-api-client/src/Entity/class-paymenttoken.php @@ -36,14 +36,19 @@ class PaymentToken { */ private $type; - /** + /** + * @var \stdClass + */ + private $source; + + /** * PaymentToken constructor. * * @param string $id The Id. * @param string $type The type. * @throws RuntimeException When the type is not valid. */ - public function __construct( string $id, string $type = self::TYPE_PAYMENT_METHOD_TOKEN ) { + public function __construct( string $id, string $type = self::TYPE_PAYMENT_METHOD_TOKEN, \stdClass $source ) { if ( ! in_array( $type, self::VALID_TYPES, true ) ) { throw new RuntimeException( __( 'Not a valid payment source type.', 'woocommerce-paypal-payments' ) @@ -51,7 +56,8 @@ class PaymentToken { } $this->id = $id; $this->type = $type; - } + $this->source = $source; + } /** * Returns the ID. @@ -71,6 +77,16 @@ class PaymentToken { return $this->type; } + /** + * Returns the source. + * + * @return \stdClass + */ + public function source(): \stdClass + { + return $this->source; + } + /** * Returns the object as array. * @@ -80,6 +96,7 @@ class PaymentToken { return array( 'id' => $this->id(), 'type' => $this->type(), + 'source' => $this->source(), ); } } diff --git a/modules/ppcp-api-client/src/Factory/class-paymenttokenfactory.php b/modules/ppcp-api-client/src/Factory/class-paymenttokenfactory.php index eac127695..897838953 100644 --- a/modules/ppcp-api-client/src/Factory/class-paymenttokenfactory.php +++ b/modules/ppcp-api-client/src/Factory/class-paymenttokenfactory.php @@ -31,9 +31,11 @@ class PaymentTokenFactory { __( 'No id for payment token given', 'woocommerce-paypal-payments' ) ); } + return new PaymentToken( $data->id, - ( isset( $data->type ) ) ? $data->type : PaymentToken::TYPE_PAYMENT_METHOD_TOKEN + ( isset( $data->type ) ) ? $data->type : PaymentToken::TYPE_PAYMENT_METHOD_TOKEN, + $data->source ); } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 85dfbcdc5..31388c9b1 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -15,6 +15,20 @@ class CheckoutBootstap { jQuery(document.body).on('updated_checkout', () => { this.render(); + + jQuery('#saved-credit-card').on('change', () => { + if(jQuery('#saved-credit-card').val() !== '') { + 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(); + this.renderer.hideButtons(this.gateway.button.wrapper); + this.renderer.hideButtons(this.gateway.messages.wrapper); + this.renderer.showButtons(this.gateway.hosted_fields.wrapper); + } + }) }); jQuery(document.body). @@ -79,4 +93,4 @@ class CheckoutBootstap { } } -export default CheckoutBootstap; \ No newline at end of file +export default CheckoutBootstap; diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 07dd078b6..33dabf86a 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -69,6 +69,7 @@ return array( $subscription_helper = $container->get( 'subscription.helper' ); $messages_apply = $container->get( 'button.helper.messages-apply' ); $environment = $container->get( 'onboarding.environment' ); + $payment_token_repository = $container->get('subscription.repository.payment-token'); return new SmartButton( $container->get( 'button.url' ), $container->get( 'session.handler' ), @@ -81,7 +82,8 @@ return array( $dcc_applies, $subscription_helper, $messages_apply, - $environment + $environment, + $payment_token_repository ); }, 'button.url' => static function ( $container ): string { diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index d1e2bc778..9df2440a8 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button\Assets; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken; +use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; @@ -22,6 +23,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; +use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -114,7 +116,12 @@ class SmartButton implements SmartButtonInterface { */ private $environment; - /** + /** + * @var PaymentTokenRepository + */ + private $payment_token_repository; + + /** * SmartButton constructor. * * @param string $module_url The URL to the module. @@ -142,7 +149,8 @@ class SmartButton implements SmartButtonInterface { DccApplies $dcc_applies, SubscriptionHelper $subscription_helper, MessagesApply $messages_apply, - Environment $environment + Environment $environment, + PaymentTokenRepository $payment_token_repository ) { $this->module_url = $module_url; @@ -157,7 +165,8 @@ class SmartButton implements SmartButtonInterface { $this->subscription_helper = $subscription_helper; $this->messages_apply = $messages_apply; $this->environment = $environment; - } + $this->payment_token_repository = $payment_token_repository; + } /** * Registers the necessary action hooks to render the HTML depending on the settings. @@ -199,15 +208,39 @@ class SmartButton implements SmartButtonInterface { 11 ); + $payment_token_repository = $this->payment_token_repository; add_filter( 'woocommerce_credit_card_form_fields', - function ( $default_fields, $id ) { + function ( $default_fields, $id ) use( $payment_token_repository ) { if ( $this->can_save_credit_card() ) { $default_fields['card-vault'] = sprintf( - '

', + '

', esc_html__( 'Save your Credit Card', 'woocommerce-paypal-payments' ) ); + + $tokens = $payment_token_repository->all_for_user_id( 1 ); + if($tokens && $this->tokens_contains_card($tokens) ) { + + $output = sprintf( + ''; + + $default_fields['saved-credit-card'] = $output; + } } + return $default_fields; }, 10, @@ -988,4 +1021,19 @@ class SmartButton implements SmartButtonInterface { return true; } + + /** + * Check if tokens has card source. + * + * @param PaymentToken[] $tokens + * @return bool + */ + protected function tokens_contains_card($tokens) { + foreach ($tokens as $token) { + if(isset($token->source()->card)) { + return true; + } + } + return false; + } } diff --git a/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php b/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php index d12735bb8..e95bc2704 100644 --- a/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php +++ b/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php @@ -72,6 +72,26 @@ class PaymentTokenRepository { } } + /** + * @param int $id + * @return array + */ + public function all_for_user_id( int $id ) { + $tokens = get_user_meta( $id, self::USER_META, true ); + if($tokens) { + return (array) $tokens; + } + + $tokens_array = []; + try { + $tokens = $this->endpoint->for_user( $id ); + update_user_meta( $id, self::USER_META, $tokens ); + return $tokens; + } catch (RuntimeException $exception) { + return []; + } + } + /** * Delete a token for a user. * diff --git a/modules/ppcp-subscription/src/class-subscriptionmodule.php b/modules/ppcp-subscription/src/class-subscriptionmodule.php index 130f688b3..d35f8d4ed 100644 --- a/modules/ppcp-subscription/src/class-subscriptionmodule.php +++ b/modules/ppcp-subscription/src/class-subscriptionmodule.php @@ -58,6 +58,24 @@ class SubscriptionModule implements ModuleInterface { 10, 2 ); + + /* + add_action('woocommerce_init', function () use($container) { + $api = $container->get('api.endpoint.payment-token' ); + try { + $tokens = $api->for_user(1); + + for($i = 0; $i < count($tokens); $i++) { + $api->delete_token($tokens[$i]); + } + + $a = 1; + } catch (RuntimeException $exception) { + $a = 1; + } + + }); + */ } /** diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 66573c176..32ec6d670 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway; use Dhii\Data\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; @@ -66,6 +67,10 @@ return array( $session_handler = $container->get( 'session.handler' ); $refund_processor = $container->get( 'wcgateway.processor.refunds' ); $state = $container->get( 'onboarding.state' ); + $payment_token_repository = $container->get('subscription.repository.payment-token'); + $purchase_unit_factory = $container->get('api.factory.purchase-unit'); + $payer_factory = $container->get('api.factory.payer'); + $order_endpoint = $container->get('api.endpoint.order'); return new CreditCardGateway( $settings_renderer, $order_processor, @@ -75,8 +80,12 @@ return array( $module_url, $session_handler, $refund_processor, - $state - ); + $state, + $payment_token_repository, + $purchase_unit_factory, + $payer_factory, + $order_endpoint + ); }, 'wcgateway.disabler' => static function ( $container ): DisableGateways { $session_handler = $container->get( 'session.handler' ); diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php index 6ba30517e..e798861b4 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php @@ -9,8 +9,12 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; @@ -41,7 +45,27 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ private $refund_processor; - /** + /** + * @var PaymentTokenRepository + */ + private $payment_token_repository; + + /** + * @var PurchaseUnitFactory + */ + private $purchase_unit_factory; + + /** + * @var PayerFactory + */ + private $payer_factory; + + /** + * @var OrderEndpoint + */ + private $order_endpoint; + + /** * CreditCardGateway constructor. * * @param SettingsRenderer $settings_renderer The Settings Renderer. @@ -63,7 +87,11 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { string $module_url, SessionHandler $session_handler, RefundProcessor $refund_processor, - State $state + State $state, + PaymentTokenRepository $payment_token_repository, + PurchaseUnitFactory $purchase_unit_factory, + PayerFactory $payer_factory, + OrderEndpoint $order_endpoint ) { $this->id = self::ID; @@ -124,7 +152,11 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { ); $this->module_url = $module_url; - } + $this->payment_token_repository = $payment_token_repository; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->payer_factory = $payer_factory; + $this->order_endpoint = $order_endpoint; + } /** * Initialize the form fields. diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php index 2af824738..c6e54eb38 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php @@ -9,6 +9,7 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -30,6 +31,56 @@ trait ProcessPaymentTrait { return null; } + /** + * If customer has chosed a saved credit card payment. + */ + $saved_credit_card = filter_input(INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING); + if($saved_credit_card) { + + $user_id = (int) $wc_order->get_customer_id(); + $customer = new \WC_Customer( $user_id ); + $tokens = $this->payment_token_repository->all_for_user_id( (int) $customer->get_id() ); + + $selected_token = null; + foreach ($tokens as $token) { + if($token->id() === $saved_credit_card) { + $selected_token = $token; + break; + } + } + + if(!$selected_token) { + return null; + } + + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); + $payer = $this->payer_factory->from_customer( $customer ); + try { + $order = $this->order_endpoint->create( + array( $purchase_unit ), + $payer, + $selected_token + ); + + if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) { + $wc_order->update_status( + 'processing', + __( 'Payment received.', 'woocommerce-paypal-payments' ) + ); + + $this->session_handler->destroy_session_data(); + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); + } + } catch (RuntimeException $error) { + $this->session_handler->destroy_session_data(); + wc_add_notice( $error->getMessage(), 'error' ); + return null; + } + } + /** * If the WC_Order is payed through the approved webhook. */