Merge branch '3d-secure'

This commit is contained in:
David Remer 2020-07-16 09:33:35 +03:00
commit cb1494f103
14 changed files with 337 additions and 22 deletions

View file

@ -55,6 +55,7 @@ document.addEventListener(
}
if (
! document.querySelector(PayPalCommerceGateway.button.wrapper) &&
! document.querySelector(PayPalCommerceGateway.button.mini_cart_wrapper) &&
! document.querySelector(PayPalCommerceGateway.hosted_fields.wrapper)
) {
return;

View file

@ -11,11 +11,14 @@ class CartActionHandler {
configuration() {
const createOrder = (data, actions) => {
const payer = payerData();
const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ?
this.config.bn_codes[this.config.context] : '';
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: this.config.ajax.create_order.nonce,
purchase_units: [],
bn_code:bnCode,
payer
}),
}).then(function(res) {
@ -30,7 +33,7 @@ class CartActionHandler {
return {
createOrder,
onApprove: onApprove(this),
onApprove: onApprove(this, this.errorHandler),
onError: (error) => {
this.errorHandler.message(error);
}

View file

@ -12,11 +12,14 @@ class CheckoutActionHandler {
const createOrder = (data, actions) => {
const payer = payerData();
const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ?
this.config.bn_codes[this.config.context] : '';
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: this.config.ajax.create_order.nonce,
payer
payer,
bn_code:bnCode
})
}).then(function (res) {
return res.json();
@ -29,7 +32,7 @@ class CheckoutActionHandler {
}
return {
createOrder,
onApprove:onApprove(this),
onApprove:onApprove(this, this.errorHandler),
onError: (error) => {
this.errorHandler.message(error);
}

View file

@ -35,7 +35,7 @@ class SingleProductActionHandler {
return {
createOrder: this.createOrder(),
onApprove: onApprove(this),
onApprove: onApprove(this, this.errorHandler),
onError: (error) => {
this.errorHandler.message(error);
}
@ -75,12 +75,15 @@ class SingleProductActionHandler {
const onResolve = (purchase_units) => {
const payer = payerData();
const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ?
this.config.bn_codes[this.config.context] : '';
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: this.config.ajax.create_order.nonce,
purchase_units,
payer
payer,
bn_code:bnCode
})
}).then(function (res) {
return res.json();

View file

@ -1,4 +1,4 @@
const onApprove = (context) => {
const onApprove = (context, errorHandler) => {
return (data, actions) => {
return fetch(context.config.ajax.approve_order.endpoint, {
method: 'POST',
@ -10,7 +10,8 @@ const onApprove = (context) => {
return res.json();
}).then((data)=>{
if (!data.success) {
throw Error(data.data);
errorHandler.message(data.data);
throw new Error(data.data);
}
location.href = context.config.redirect;
});

View file

@ -10,7 +10,8 @@ const onApprove = (context) => {
return res.json();
}).then((data)=>{
if (!data.success) {
throw Error(data.data);
errorHandler.message(data.data);
throw new Error(data.data);
}
document.querySelector('#place_order').click()
});

View file

@ -47,15 +47,6 @@ class CreditCardRenderer {
contingencies: ['3D_SECURE']
}).then((payload) => {
payload.orderID = payload.orderId;
console.log(payload);
if (payload.liabilityShift === 'POSSIBLE') {
return contextConfig.onApprove(payload);
}
if (payload.liabilityShift) {
return contextConfig.onApprove(payload);
}
return contextConfig.onApprove(payload);
});
}

View file

@ -13,6 +13,7 @@ use Inpsyde\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\RequestData;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
use Inpsyde\PayPalCommerce\Button\Helper\ThreeDSecure;
use Inpsyde\PayPalCommerce\Onboarding\Environment;
use Inpsyde\PayPalCommerce\Onboarding\State;
@ -91,12 +92,17 @@ return [
$repository = $container->get('api.repository.cart');
$apiClient = $container->get('api.endpoint.order');
$payerFactory = $container->get('api.factory.payer');
return new CreateOrderEndpoint($requestData, $repository, $apiClient, $payerFactory);
$sessionHandler = $container->get('session.handler');
return new CreateOrderEndpoint($requestData, $repository, $apiClient, $payerFactory, $sessionHandler);
},
'button.endpoint.approve-order' => static function (ContainerInterface $container): ApproveOrderEndpoint {
$requestData = $container->get('button.request-data');
$apiClient = $container->get('api.endpoint.order');
$sessionHandler = $container->get('session.handler');
return new ApproveOrderEndpoint($requestData, $apiClient, $sessionHandler);
$threeDSecure = $container->get('button.endpoint.helper.three-d-secure');
return new ApproveOrderEndpoint($requestData, $apiClient, $sessionHandler, $threeDSecure);
},
'button.endpoint.helper.three-d-secure' => static function (ContainerInterface $container): ThreeDSecure {
return new ThreeDSecure();
},
];

View file

@ -236,6 +236,7 @@ class SmartButton implements SmartButtonInterface
'nonce' => wp_create_nonce(ApproveOrderEndpoint::nonce()),
],
],
'bn_codes' => $this->bnCodes(),
'payer' => $this->payerData(),
'button' => [
'wrapper' => '#ppc-button',
@ -310,7 +311,9 @@ class SmartButton implements SmartButtonInterface
private function attributes(): array
{
$attributes = [];
$attributes = [
'data-partner-attribution-id' => $this->bnCodeForContext($this->context()),
];
try {
$clientToken = $this->identityToken->generate();
$attributes['data-client-token'] = $clientToken->token();
@ -320,6 +323,33 @@ class SmartButton implements SmartButtonInterface
}
}
/**
* @param string $context
* @return string
*/
private function bnCodeForContext(string $context): string
{
$codes = $this->bnCodes();
return (isset($codes[$context])) ? $codes[$context] : '';
}
/**
* BN Codes
*
* @return array
*/
private function bnCodes(): array
{
return [
'checkout' => 'Woo_PPCP',
'cart' => 'Woo_PPCP',
'mini-cart' => 'Woo_PPCP',
'product' => 'Woo_PPCP',
];
}
private function components(): array
{
$components = ['buttons'];

View file

@ -5,8 +5,10 @@ declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Endpoint;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
use Inpsyde\PayPalCommerce\ApiClient\Entity\OrderStatus;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
use Inpsyde\PayPalCommerce\Button\Helper\ThreeDSecure;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
class ApproveOrderEndpoint implements EndpointInterface
@ -17,15 +19,18 @@ class ApproveOrderEndpoint implements EndpointInterface
private $requestData;
private $sessionHandler;
private $apiEndpoint;
private $threedSecure;
public function __construct(
RequestData $requestData,
OrderEndpoint $apiEndpoint,
SessionHandler $sessionHandler
SessionHandler $sessionHandler,
ThreeDSecure $threedSecure
) {
$this->requestData = $requestData;
$this->apiEndpoint = $apiEndpoint;
$this->sessionHandler = $sessionHandler;
$this->threedSecure = $threedSecure;
}
public static function nonce(): string
@ -52,6 +57,23 @@ class ApproveOrderEndpoint implements EndpointInterface
);
}
if ($order->paymentSource() && $order->paymentSource()->card()) {
$proceed = $this->threedSecure->proceedWithOrder($order);
if ($proceed === ThreeDSecure::RETRY) {
throw new RuntimeException(
__('Something went wrong. Please try again.', 'woocommerce-paypal-commerce-gateway')
);
}
if ($proceed === ThreeDSecure::REJECT) {
throw new RuntimeException(
__(
'Unfortunatly, we can\'t accept your card. Please choose a different payment method.',
'woocommerce-paypal-commerce-gateway'
)
);
}
}
if (! $order->status()->is(OrderStatus::APPROVED)) {
throw new RuntimeException(
sprintf(

View file

@ -8,6 +8,7 @@ use Inpsyde\PayPalCommerce\ApiClient\Factory\PayerFactory;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
use Inpsyde\PayPalCommerce\ApiClient\Repository\CartRepository;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
class CreateOrderEndpoint implements EndpointInterface
{
@ -18,17 +19,20 @@ class CreateOrderEndpoint implements EndpointInterface
private $repository;
private $apiEndpoint;
private $payerFactory;
private $sessionHandler;
public function __construct(
RequestData $requestData,
CartRepository $repository,
OrderEndpoint $apiEndpoint,
PayerFactory $payerFactory
PayerFactory $payerFactory,
SessionHandler $sessionHandler
) {
$this->requestData = $requestData;
$this->repository = $repository;
$this->apiEndpoint = $apiEndpoint;
$this->payerFactory = $payerFactory;
$this->sessionHandler = $sessionHandler;
}
public static function nonce(): string
@ -52,6 +56,11 @@ class CreateOrderEndpoint implements EndpointInterface
}
$payer = $this->payerFactory->fromPayPalResponse(json_decode(json_encode($data['payer'])));
}
$bnCode = isset($data['bn_code']) ? (string) $data['bn_code'] : '';
if ($bnCode) {
$this->sessionHandler->replaceBnCode($bnCode);
$this->apiEndpoint->withBnCode($bnCode);
}
$order = $this->apiEndpoint->createForPurchaseUnits(
$purchaseUnits,
$payer

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Helper;
use Inpsyde\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
class ThreeDSecure
{
public const NO_DECISION = 0;
public const PROCCEED = 1;
public const REJECT = 2;
public const RETRY = 3;
/**
* Determine, how we proceed with a given order.
*
* @link https://developer.paypal.com/docs/business/checkout/add-capabilities/3d-secure/#authenticationresult
* @param Order $order
* @return int
*/
public function proceedWithOrder(Order $order): int
{
if (! $order->paymentSource()) {
return self::NO_DECISION;
}
if (! $order->paymentSource()->card()) {
return self::NO_DECISION;
}
if (! $order->paymentSource()->card()->authenticationResult()) {
return self::NO_DECISION;
}
$result = $order->paymentSource()->card()->authenticationResult();
if ($result->liabilityShift() === CardAuthenticationResult::LIABILITY_SHIFT_POSSIBLE) {
return self::PROCCEED;
}
if ($result->liabilityShift() === CardAuthenticationResult::LIABILITY_SHIFT_UNKNOWN) {
return self::RETRY;
}
if ($result->liabilityShift() === CardAuthenticationResult::LIABILITY_SHIFT_NO) {
return $this->noLiabilityShift($result);
}
return self::NO_DECISION;
}
/**
* @return int
*/
private function noLiabilityShift(CardAuthenticationResult $result): int
{
if (
$result->enrollmentStatus() === CardAuthenticationResult::ENROLLMENT_STATUS_BYPASS
&& ! $result->authenticationResult()
) {
return self::PROCCEED;
}
if (
$result->enrollmentStatus() === CardAuthenticationResult::ENROLLMENT_STATUS_UNAVAILABLE
&& ! $result->authenticationResult()
) {
return self::PROCCEED;
}
if (
$result->enrollmentStatus() === CardAuthenticationResult::ENROLLMENT_STATUS_NO
&& ! $result->authenticationResult()
) {
return self::PROCCEED;
}
if ($result->authenticationResult() === CardAuthenticationResult::AUTHENTICATION_RESULT_REJECTED) {
return self::REJECT;
}
if ($result->authenticationResult() === CardAuthenticationResult::AUTHENTICATION_RESULT_NO) {
return self::REJECT;
}
if ($result->authenticationResult() === CardAuthenticationResult::AUTHENTICATION_RESULT_UNABLE) {
return self::RETRY;
}
if (! $result->authenticationResult()) {
return self::RETRY;
}
return self::NO_DECISION;
}
}

View file

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
use Inpsyde\PayPalCommerce\WcGateway\Settings\Settings;
use Inpsyde\Woocommerce\Logging\Logger\NullLogger;
use Inpsyde\Woocommerce\Logging\Logger\WooCommerceLogger;
use Psr\Container\ContainerInterface;
@ -32,6 +35,33 @@ return [
$settings = $container->get('wcgateway.settings');
return $settings->has('client_secret') ? (string) $settings->get('client_secret') : '';
},
'api.endpoint.order' => static function (ContainerInterface $container): OrderEndpoint {
$orderFactory = $container->get('api.factory.order');
$patchCollectionFactory = $container->get('api.factory.patch-collection-factory');
$logger = $container->get('woocommerce.logger.woocommerce');
/**
* @var SessionHandler $sessionHandler
*/
$sessionHandler = $container->get('session.handler');
$bnCode = $sessionHandler->bnCode();
/**
* @var Settings $settings
*/
$settings = $container->get('wcgateway.settings');
$intent = $settings->has('intent') && strtoupper((string) $settings->get('intent')) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE';
$applicationContextRepository = $container->get('api.repository.application-context');
return new OrderEndpoint(
$container->get('api.host'),
$container->get('api.bearer'),
$orderFactory,
$patchCollectionFactory,
$intent,
$logger,
$applicationContextRepository,
$bnCode
);
},
'woocommerce.logger.woocommerce' => function (ContainerInterface $container): LoggerInterface {
$settings = $container->get('wcgateway.settings');
if (! $settings->has('logging_enabled') || ! $settings->get('logging_enabled')) {

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Helper;
use Inpsyde\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
use Inpsyde\PayPalCommerce\ApiClient\Entity\PaymentSource;
use Inpsyde\PayPalCommerce\ApiClient\Entity\PaymentSourceCard;
use Inpsyde\PayPalCommerce\TestCase;
use Mockery\Mock;
class ThreeDSecureTest extends TestCase
{
/**
* @dataProvider dataForTestDefault
* @param Order $order
* @param int $expected
*/
public function testDefault(int $expected, string $liabilityShift, string $authenticationResult, string $enrollment)
{
$result = \Mockery::mock(CardAuthenticationResult::class);
$result->shouldReceive('liabilityShift')->andReturn($liabilityShift);
$result->shouldReceive('authenticationResult')->andReturn($authenticationResult);
$result->shouldReceive('enrollmentStatus')->andReturn($enrollment);
$card = \Mockery::mock(PaymentSourceCard::class);
$card->shouldReceive('authenticationResult')->andReturn($result);
$source = \Mockery::mock(PaymentSource::class);
$source->shouldReceive('card')->andReturn($card);
$order = \Mockery::mock(Order::class);
$order->shouldReceive('paymentSource')->andReturn($source);
$testee = new ThreeDSecure();
$result = $testee->proceedWithOrder($order);
$this->assertEquals($expected, $result);
}
public function dataForTestDefault() : array
{
$matrix = [
'test_1' => [
ThreeDSecure::PROCCEED,
CardAuthenticationResult::LIABILITY_SHIFT_POSSIBLE,
CardAuthenticationResult::AUTHENTICATION_RESULT_YES,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_2' => [
ThreeDSecure::REJECT,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
CardAuthenticationResult::AUTHENTICATION_RESULT_NO,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_3' => [
ThreeDSecure::REJECT,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
CardAuthenticationResult::AUTHENTICATION_RESULT_REJECTED,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_4' => [
ThreeDSecure::PROCCEED,
CardAuthenticationResult::LIABILITY_SHIFT_POSSIBLE,
CardAuthenticationResult::AUTHENTICATION_RESULT_ATTEMPTED,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_5' => [
ThreeDSecure::RETRY,
CardAuthenticationResult::LIABILITY_SHIFT_UNKNOWN,
CardAuthenticationResult::AUTHENTICATION_RESULT_UNABLE,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_6' => [
ThreeDSecure::RETRY,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
CardAuthenticationResult::AUTHENTICATION_RESULT_UNABLE,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_7' => [
ThreeDSecure::RETRY,
CardAuthenticationResult::LIABILITY_SHIFT_UNKNOWN,
CardAuthenticationResult::AUTHENTICATION_RESULT_CHALLENGE_REQUIRED,
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_8' => [
ThreeDSecure::RETRY,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
'',
CardAuthenticationResult::ENROLLMENT_STATUS_YES,
],
'test_9' => [
ThreeDSecure::PROCCEED,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
'',
CardAuthenticationResult::ENROLLMENT_STATUS_NO,
],
'test_10' => [
ThreeDSecure::PROCCEED,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
'',
CardAuthenticationResult::ENROLLMENT_STATUS_UNAVAILABLE,
],
'test_11' => [
ThreeDSecure::RETRY,
CardAuthenticationResult::LIABILITY_SHIFT_UNKNOWN,
'',
CardAuthenticationResult::ENROLLMENT_STATUS_UNAVAILABLE,
],
'test_12' => [
ThreeDSecure::PROCCEED,
CardAuthenticationResult::LIABILITY_SHIFT_NO,
'',
CardAuthenticationResult::ENROLLMENT_STATUS_BYPASS,
],
'test_13' => [
ThreeDSecure::RETRY,
CardAuthenticationResult::LIABILITY_SHIFT_UNKNOWN,
'',
'',
],
];
return $matrix;
}
}