We will be off from 27/1 (Monday) to 31/1 (Friday) (GMT +7) for our Tet Holiday (Lunar New Year) in our country

Commit 245987bd authored by attilak's avatar attilak

[WIP]

resolve code review comments
use adyen-3ds2-js-utils helper function
component js updated to v2.2.1
parent d6e0c615
......@@ -27,8 +27,8 @@ namespace Adyen\Payment\Api;
interface AdyenThreeDS2ProcessInterface
{
/**
* @param mixed $payload
* @return mixed
* @param string $payload
* @return string
*/
public function initiate($payload);
}
......@@ -56,7 +56,7 @@ class TransactionPayment implements ClientInterface
{
$request = $transferObject->getBody();
// If the payments call is already done return the
// If the payments call is already done return the request
if (!empty($request['resultCode'])) {
//Initiate has already a response
return $request;
......
......@@ -44,6 +44,7 @@ class AddressDataBuilder implements BuilderInterface
* AddressDataBuilder constructor.
*
* @param \Adyen\Payment\Helper\Data $adyenHelper
* @param \Adyen\Payment\Helper\Requests $adyenRequestsHelper
*/
public function __construct(
\Adyen\Payment\Helper\Data $adyenHelper,
......
......@@ -24,7 +24,6 @@
namespace Adyen\Payment\Gateway\Request;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Adyen\Payment\Observer\AdyenCcDataAssignObserver;
class CcAuthorizationDataBuilder implements BuilderInterface
{
......@@ -57,9 +56,14 @@ class CcAuthorizationDataBuilder implements BuilderInterface
// retrieve payments response which we already got and saved in the
// Adyen\Payment\Plugin\PaymentInformationManagement::afterSavePaymentInformation
if ($response = $payment->getAdditionalInformation("paymentsResponse")) {
// the payments response needs to be passed to the next process because after this point we don't have
// access to the payment object therefore to the additionalInformation array
$request = $response;
// Remove from additional data
$payment->unsAdditionalInformation("paymentsResponse");
// TODO check if qoupte needs to be saved or not
} else {
$errorMsg = __('Error with payment method please select different payment method.');
throw new \Magento\Framework\Exception\LocalizedException(__($errorMsg));
......
......@@ -27,22 +27,14 @@ use Magento\Payment\Gateway\Validator\AbstractValidator;
class ThreeDS2ResponseValidator extends AbstractValidator
{
/**
* @var \Adyen\Payment\Logger\AdyenLogger
*/
private $adyenLogger;
/**
* GeneralResponseValidator constructor.
*
* @param \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory
* @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger
*/
public function __construct(
\Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory,
\Adyen\Payment\Logger\AdyenLogger $adyenLogger
\Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory
) {
$this->adyenLogger = $adyenLogger;
parent::__construct($resultFactory);
}
......
......@@ -35,8 +35,8 @@ class Data extends AbstractHelper
const LIVE = 'live';
const CHECKOUT_CONTEXT_URL_LIVE = 'https://checkoutshopper-live.adyen.com/checkoutshopper/';
const CHECKOUT_CONTEXT_URL_TEST = 'https://checkoutshopper-test.adyen.com/checkoutshopper/';
const CHECKOUT_COMPONENT_JS_LIVE = 'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/2.2.0/adyen.js';
const CHECKOUT_COMPONENT_JS_TEST = 'https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/2.2.0/adyen.js';
const CHECKOUT_COMPONENT_JS_LIVE = 'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/2.2.1/adyen.js';
const CHECKOUT_COMPONENT_JS_TEST = 'https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/2.2.1/adyen.js';
/**
* @var \Magento\Framework\Encryption\EncryptorInterface
......@@ -118,6 +118,9 @@ class Data extends AbstractHelper
*/
private $agreementResourceModel;
/**
* @var \Magento\Framework\Locale\ResolverInterface
*/
private $localeResolver;
/**
......@@ -442,7 +445,7 @@ class Data extends AbstractHelper
* Gives back adyen_cc_vault configuration values as flag
*
* @param $field
* @param null $storeId
* @param int|null $storeId
* @return mixed
*/
public function getAdyenCcVaultConfigDataFlag($field, $storeId = null)
......
......@@ -155,8 +155,7 @@ class Requests extends AbstractHelper
// Save the defaults for later to compare if anything has changed
$requestBilling = $requestBillingDefaults;
// Parse address into street and house number where possible
$address = $this->adyenHelper->getStreetFromString($billingAddress->getStreetFull());
$address = $this->getStreetStringFromBillingAddress($billingAddress);
if (!empty($address["name"])) {
$requestBilling["street"] = $address["name"];
......@@ -290,7 +289,7 @@ class Requests extends AbstractHelper
$request['browserInfo']['colorDepth'] = $payment->getAdditionalInformation(AdyenCcDataAssignObserver::SCREEN_COLOR_DEPTH);
$request['browserInfo']['timeZoneOffset'] = $payment->getAdditionalInformation(AdyenCcDataAssignObserver::TIMEZONE_OFFSET);
$request['browserInfo']['language'] = $this->adyenHelper->getCurrentLocaleCode($store);
$request['browserInfo']['javaEnabled'] = true; //$payment->getAdditionalInformation(AdyenCcDataAssignObserver::JAVA_ENABLED);
$request['browserInfo']['javaEnabled'] = $payment->getAdditionalInformation(AdyenCcDataAssignObserver::JAVA_ENABLED);
// uset browser related data from additional information
$payment->unsAdditionalInformation(AdyenCcDataAssignObserver::SCREEN_WIDTH);
......@@ -416,4 +415,24 @@ class Requests extends AbstractHelper
return $request;
}
/**
* The billing address retrieved from the Quote and the one retrieved from the Order has some differences
* Therefore we need to check if the getStreetFull function exists and use that if yes, otherwise use the more
* commont getStreetLine1
*
* @param $billingAddress
* @return array
*/
private function getStreetStringFromBillingAddress($billingAddress)
{
if (method_exists($billingAddress, 'getStreetFull')) {
// Parse address into street and house number where possible
$address = $this->adyenHelper->getStreetFromString($billingAddress->getStreetFull());
} else {
$address = $this->adyenHelper->getStreetFromString($billingAddress->getStreetLine1());
}
return $address;
}
}
......@@ -54,14 +54,15 @@ class AdyenThreeDS2Process implements AdyenThreeDS2ProcessInterface
/**
* @api
* @param mixed $payload
* @return mixed
* @param string $payload
* @return string
*/
public function initiate($payload)
{
// Decode payload from frontend
$payload = json_decode($payload, true);
// Validate JSON that has just been parsed if it was in a valid format
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Magento\Framework\Exception\LocalizedException(__('3D secure 2.0 failed because the request was not a valid JSON'));
}
......@@ -71,12 +72,19 @@ class AdyenThreeDS2Process implements AdyenThreeDS2ProcessInterface
$payment = $quote->getPayment();
// Init payments/details request
$result = [];
if ($paymentData = $payment->getAdditionalInformation("threeDS2PaymentData")) {
// Add payment data into the request object
$request = [
"paymentData" => $payment->getAdditionalInformation("threeDS2PaymentData")
];
// unset payment data from additional information
$payment->unsAdditionalInformation("threeDS2PaymentData");
} else {
throw new \Magento\Framework\Exception\LocalizedException(__('3D secure 2.0 failed, payment data not found'));
}
// Depends on the component's response we send a fingerprint or the challenge result
if (!empty($payload['details']['threeds2.fingerprint'])) {
......@@ -109,8 +117,15 @@ class AdyenThreeDS2Process implements AdyenThreeDS2ProcessInterface
}
// Payment can get back to the original flow
// Save the payments response because we are going to need it during the place order flow
$payment->setAdditionalInformation("paymentsResponse", $result);
// Setting the placeOrder to true enables the process to skip the payments call because the paymentsResponse
// is already in place - only set placeOrder to true when you have the paymentsResponse
$payment->setAdditionalInformation('placeOrder', true);
// To actually save the additional info changes into the quote
$quote->save();
// 3DS2 flow is done, original place order flow can continue from frontend
......
......@@ -29,15 +29,58 @@ use Magento\Vault\Model\Ui\VaultConfigProvider;
class PaymentInformationManagement
{
/**
* @var \Magento\Checkout\Model\Session
*/
private $checkoutSession;
/**
* @var \Adyen\Payment\Helper\Data
*/
private $adyenHelper;
/**
* @var \Adyen\Payment\Helper\Requests
*/
private $adyenRequestHelper;
/**
* @var \Magento\Framework\Model\Context
*/
private $context;
/**
* @var \Adyen\Payment\Gateway\Http\TransferFactory
*/
private $transferFactory;
/**
* @var \Adyen\Payment\Gateway\Http\Client\TransactionPayment
*/
private $transactionPayment;
/**
* @var \Adyen\Payment\Gateway\Validator\CheckoutResponseValidator
*/
private $checkoutResponseValidator;
/**
* @var \Adyen\Payment\Gateway\Validator\ThreeDS2ResponseValidator
*/
private $threeDS2ResponseValidator;
/**
* PaymentInformationManagement constructor.
*
* @param \Magento\Checkout\Model\Session $checkoutSession
* @param \Adyen\Payment\Helper\Data $adyenHelper
* @param \Adyen\Payment\Helper\Requests $adyenRequestHelper
* @param \Magento\Framework\Model\Context $context
* @param \Adyen\Payment\Gateway\Http\TransferFactory $transferFactory
* @param \Adyen\Payment\Gateway\Http\Client\TransactionPayment $transactionPayment
* @param \Adyen\Payment\Gateway\Validator\CheckoutResponseValidator $checkoutResponseValidator
* @param \Adyen\Payment\Gateway\Validator\ThreeDS2ResponseValidator $threeDS2ResponseValidator
*/
public function __construct(
\Magento\Checkout\Model\Session $checkoutSession,
\Adyen\Payment\Helper\Data $adyenHelper,
......@@ -77,7 +120,12 @@ class PaymentInformationManagement
if ($payment->getAdditionalInformation('placeOrder')) {
$payment->unsAdditionalInformation('placeOrder');
$quote->save();
return true;
return json_encode(
array(
'threeDS2' => false
)
);
}
// Init request array
......@@ -148,16 +196,19 @@ class PaymentInformationManagement
"token" => $payment->getAdditionalInformation('threeDS2Token')
)
);
} else {
// TODO Handle error
}
} else {
$payment->setAdditionalInformation('paymentsResponse', $paymentsResponse);
$quote->save();
}
// Save the payments response because we are going to need it during the place order flow
$payment->setAdditionalInformation("paymentsResponse", $paymentsResponse);
// Setting the placeOrder to true enables the process to skip the payments call because the paymentsResponse
// is already in place - only set placeOrder to true when you have the paymentsResponse
$payment->setAdditionalInformation('placeOrder', true);
// To actually save the additional info changes into the quote
$quote->save();
// Original flow can continue, return to frontend and place the order
return json_encode(
array(
......
!function(e,n){"object"===typeof exports&&"object"===typeof module?module.exports=n():"function"===typeof define&&define.amd?define([],n):"object"===typeof exports?exports.ThreedDS2Utils=n():e.ThreedDS2Utils=n()}(this,function(){return function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){"use strict";t.r(n);var r={container:void 0},o={"01":["250px","400px"],"02":["390px","400px"],"03":["500px","600px"],"04":["600px","400px"],"05":["100%","100%"]};function a(e){return o.hasOwnProperty(e)?e:"01"}var i={createIframe:function(e,n){var t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"0",o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"0",a=arguments.length>4?arguments[4]:void 0;if(!n||0===n.length)throw new Error("Name parameter missing for iframe");e instanceof HTMLElement?r.container=e:r.container=document.body;var i=document.createElement("iframe");i.classList.add(n+"Class"),i.width=t,i.height=o,i.name=n,i.setAttribute("frameborder","0"),i.setAttribute("border","0");var d=document.createTextNode("<p>Your browser does not support iframes.</p>");return i.appendChild(d),r.container.appendChild(i),function(e,n){e.attachEvent?e.attachEvent("onload",function(){n&&"function"===typeof n&&n(e.contentWindow)}):e.onload=function(){n&&"function"===typeof n&&n(e.contentWindow)}}(i,a),i},createForm:function(e,n,t,r,o){if(!e||!n||!t||!r||!o)throw new Error("Not all required parameters provided for form creation");if(0===e.length||0===n.length||0===t.length||0===r.length||0===o.length)throw new Error("Not all required parameters have suitable values");var a=document.createElement("form");a.style.display="none",a.name=e,a.action=n,a.method="POST",a.target=t;var i=document.createElement("input");return i.name=r,i.value=o,a.appendChild(i),a},getBrowserInfo:function(){var e=window&&window.screen?window.screen.width:"",n=window&&window.screen?window.screen.height:"",t=window&&window.screen?window.screen.colorDepth:"",r=window&&window.navigator?window.navigator.userAgent:"",o=!(!window||!window.navigator)&&navigator.javaEnabled(),a="";return window&&window.navigator&&(a=window.navigator.language?window.navigator.language:window.navigator.browserLanguage),{screenWidth:e,screenHeight:n,colorDepth:t,userAgent:r,timeZoneOffset:(new Date).getTimezoneOffset(),language:a,javaEnabled:o}},base64Url:{encode:function(e){var n=window.btoa(e).split("=")[0];return n=(n=n.replace("/+/g","-")).replace("///g","_")},decode:function(e){var n=e;switch((n=(n=n.replace("/-/g","+")).replace("/_/g","/")).length%4){case 0:break;case 2:n+="==";break;case 3:n+="=";break;default:window.console&&window.console.log&&window.console.log("### base64url::decodeBase64URL:: Illegal base64url string!")}try{return window.atob(n)}catch(e){throw new Error(e)}}},config:{challengeWindowSizes:o,validateChallengeWindowSize:a,getChallengeWindowSize:function(e){return o[a(e)]},THREEDS_METHOD_TIMEOUT:1e4,CHALLENGE_TIMEOUT:6e5}};n.default=i}]).default});
\ No newline at end of file
......@@ -35,9 +35,10 @@ define(
'mage/storage',
'Magento_Checkout/js/model/full-screen-loader',
'Magento_Paypal/js/action/set-payment-method',
'Magento_Checkout/js/action/select-payment-method'
'Magento_Checkout/js/action/select-payment-method',
'Adyen_Payment/js/threeds2-js-utils'
],
function ($, ko, Component, customer, creditCardData, additionalValidators, quote, installments, url, VaultEnabler, urlBuilder, storage, fullScreenLoader, setPaymentMethodAction, selectPaymentMethodAction) {
function ($, ko, Component, customer, creditCardData, additionalValidators, quote, installments, url, VaultEnabler, urlBuilder, storage, fullScreenLoader, setPaymentMethodAction, selectPaymentMethodAction, threeDS2Utils) {
'use strict';
......@@ -48,7 +49,7 @@ define(
defaults: {
template: 'Adyen_Payment/payment/cc-form',
creditCardOwner: '',
setStoreCc: false,
storeCc: false,
installment: '',
creditCardDetailsValid: false
},
......@@ -121,7 +122,7 @@ define(
onChange: function (state, component) {
if (!!state.isValid && !component.state.errors.encryptedSecurityCode) {
self.setStoreCc = !!state.data.storeDetails;
self.storeCc = !!state.data.storeDetails;
self.variant(state.brand);
self.creditCardNumber(state.data.encryptedCardNumber);
self.expiryMonth(state.data.encryptedExpiryMonth);
......@@ -190,6 +191,11 @@ define(
},
/**
* Rendering the 3DS2.0 components
* To do the device fingerprint at the response of IdentifyShopper render the threeDS2DeviceFingerprint
* component
* To render the challenge for the customer at the response of ChallengeShopper render the
* threeDS2Challenge component
* Both of them is going to be rendered in a Magento dialog popup
*
* @param type
* @param token
......@@ -205,7 +211,7 @@ define(
fingerprintToken: token,
onComplete: function(result) {
self.processThreeDS2(result.data);
$('#modal_content').modal("closeModal");
$('#threeDS2Modal').modal("closeModal");
},
onError: function(result) {
// TODO error handling show error message
......@@ -223,7 +229,7 @@ define(
challengeToken: token,
onComplete: function(result) {
self.processThreeDS2(result.data);
$('#modal_content').modal("closeModal");
$('#threeDS2Modal').modal("closeModal");
},
onError: function(result) {
// TODO error handling show error message
......@@ -235,7 +241,9 @@ define(
self.threeDS2Component.mount(threeDS2Node);
},
/**
*
* The results that the 3DS2 components returns in the onComplete callback needs to be sent to the
* backend to the /adyen/threeDS2Process endpoint and based on the response render a new threeDS2
* component or place the order (validateThreeDS2OrPlaceOrder)
* @param response
*/
processThreeDS2: function(data) {
......@@ -264,6 +272,8 @@ define(
* @returns {{method: *, additional_data: {cc_type: *, number: *, cvc, expiryMonth: *, expiryYear: *, holderName: *, store_cc: *, number_of_installments: *}}}
*/
getData: function () {
var browserInfo = threeDS2Utils.getBrowserInfo();
var data = {
'method': this.item.method,
additional_data: {
......@@ -274,13 +284,13 @@ define(
'expiryMonth': this.expiryMonth(),
'expiryYear': this.expiryYear(),
'holderName': this.creditCardOwner(),
'store_cc': this.setStoreCc,
'store_cc': this.storeCc,
'number_of_installments': this.installment(),
'java_enabled': navigator.javaEnabled().toString(),
'screen_color_depth': screen.colorDepth,
'screen_width': screen.width,
'screen_height': screen.height,
'timezone_offset': new Date().getTimezoneOffset()
'java_enabled': browserInfo.javaEnabled,
'screen_color_depth': browserInfo.colorDepth,
'screen_width': browserInfo.screenWidth,
'screen_height': browserInfo.screenHeight,
'timezone_offset': browserInfo.timeZoneOffset
}
};
this.vaultEnabler.visitAdditionalData(data);
......@@ -310,15 +320,20 @@ define(
}
if (this.validate() && additionalValidators.validate()) {
//this.isPlaceOrderActionAllowed(false);
fullScreenLoader.startLoader();
self.isPlaceOrderActionAllowed(false);
//update payment method information if additional data was changed
selectPaymentMethodAction(this.getData());
// here I can remove all the data collected from the card component
// OR I should create a new getData function which just retrieves the data necessary for the 3ds2 flow
setPaymentMethodAction(this.messageContainer).done(
function (responseJSON) {
fullScreenLoader.stopLoader();
self.isPlaceOrderActionAllowed(true);
self.validateThreeDS2OrPlaceOrder(responseJSON);
});
return false;
......@@ -327,26 +342,24 @@ define(
return false;
},
/**
*
* Based on the response we can start a 3DS2 validation or place the order
* @param responseJSON
*/
validateThreeDS2OrPlaceOrder: function(responseJSON) {
var self = this;
console.log(responseJSON);
var response = JSON.parse(responseJSON);
if (!!response.threeDS2) {
$('#modal_content').modal({
$('#threeDS2Modal').modal({
// disable user to hide popup
clickableOverlay: false,
// empty buttons, we don't need that
buttons: []
});
$('#modal_content').modal("openModal");
$('#threeDS2Modal').modal("openModal");
// render component
self.renderThreeDS2Component(response.type, response.token);
......
......@@ -77,7 +77,7 @@
<div afterRender="renderSecureFields()" data-bind="attr: { id: 'cardContainer'}"></div>
</div>
<div id="modal_content">
<div id="threeDS2Modal">
<div id="threeDS2Container"></div>
</div>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment