Merge branch 'feature/PCP-88-payment-token-vaulting' of github.com:woocommerce/woocommerce-paypal-payments into feature/PCP-88-payment-token-vaulting

This commit is contained in:
Kirill Braslavsky 2021-03-25 17:17:39 +02:00
commit c8aa347fc2
10 changed files with 229 additions and 16 deletions

View file

@ -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(),
);
}
}

View file

@ -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
);
}

View file

@ -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;
export default CheckoutBootstap;

View file

@ -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 {

View file

@ -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(
'<p class="form-row form-row-wide"><label for="vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%1$s</label></p>',
'<p class="form-row form-row-wide"><label for="vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
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(
'<select id="saved-credit-card" name="saved_credit_card"><option value="">%s</option>',
esc_html__( 'Choose a saved payment', 'woocommerce-paypal-payments' )
);
foreach ($tokens as $token) {
if(isset($token->source()->card)) {
$output .= sprintf(
'<option value="%1$s">%2$s ...%3$s</option>',
$token->id(),
$token->source()->card->brand,
$token->source()->card->last_digits
);
}
}
$output .= '</select>';
$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;
}
}

View file

@ -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.
*

View file

@ -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;
}
});
*/
}
/**

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
@ -65,6 +66,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,
@ -74,8 +79,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' );

View file

@ -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.

View file

@ -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.
*/