diff --git a/app/assets/javascripts/solidus_paypal_braintree/checkout.js b/app/assets/javascripts/solidus_paypal_braintree/checkout.js
index bebba34f..a0e24df0 100644
--- a/app/assets/javascripts/solidus_paypal_braintree/checkout.js
+++ b/app/assets/javascripts/solidus_paypal_braintree/checkout.js
@@ -44,16 +44,38 @@ $(function() {
var $nonce = $("#payment_method_nonce", $field);
if ($nonce.length > 0 && $nonce.val() === "") {
+ var client = braintreeForm._merchantConfigurationOptions._solidusClient;
+
event.preventDefault();
disableSubmit();
braintreeForm.tokenize(function(error, payload) {
if (error) {
braintreeError(error);
- } else {
- $nonce.val(payload.nonce);
+ return;
+ }
+
+ $nonce.val(payload.nonce);
+
+ if (!client.useThreeDSecure) {
$paymentForm.submit();
+ return;
+ }
+
+ threeDSecureOptions.nonce = payload.nonce;
+ threeDSecureOptions.bin = payload.details.bin;
+ threeDSecureOptions.onLookupComplete = function(data, next) {
+ next();
}
+ client._threeDSecureInstance.verifyCard(threeDSecureOptions, function(error, response) {
+ if (error === null && (!response.liabilityShiftPossible || response.liabilityShifted)) {
+ $nonce.val(response.nonce);
+ $paymentForm.submit();
+ } else {
+ $nonce.val('');
+ braintreeError(error || { code: 'THREEDS_AUTHENTICATION_FAILED' });
+ }
+ });
});
}
}
diff --git a/app/assets/javascripts/solidus_paypal_braintree/client.js b/app/assets/javascripts/solidus_paypal_braintree/client.js
index 723ed512..29956efb 100644
--- a/app/assets/javascripts/solidus_paypal_braintree/client.js
+++ b/app/assets/javascripts/solidus_paypal_braintree/client.js
@@ -57,10 +57,12 @@ SolidusPaypalBraintree.Client = function(config) {
this.useDataCollector = config.useDataCollector;
this.usePaypal = config.usePaypal;
this.useApplepay = config.useApplepay;
+ this.useThreeDSecure = config.useThreeDSecure;
this._braintreeInstance = null;
this._dataCollectorInstance = null;
this._paypalInstance = null;
+ this._threeDSecureInstance = null;
};
/**
@@ -71,18 +73,22 @@ SolidusPaypalBraintree.Client.prototype.initialize = function() {
var initializationPromise = this._fetchToken().
then(this._createBraintreeInstance.bind(this));
- if(this.useDataCollector) {
+ if (this.useDataCollector) {
initializationPromise = initializationPromise.then(this._createDataCollector.bind(this));
}
- if(this.usePaypal) {
+ if (this.usePaypal) {
initializationPromise = initializationPromise.then(this._createPaypal.bind(this));
}
- if(this.useApplepay) {
+ if (this.useApplepay) {
initializationPromise = initializationPromise.then(this._createApplepay.bind(this));
}
+ if (this.useThreeDSecure) {
+ initializationPromise = initializationPromise.then(this._createThreeDSecure.bind(this));
+ }
+
return initializationPromise.then(this._invokeReadyCallback.bind(this));
};
@@ -186,3 +192,14 @@ SolidusPaypalBraintree.Client.prototype._createApplepay = function() {
return applePayInstance;
}.bind(this));
};
+
+SolidusPaypalBraintree.Client.prototype._createThreeDSecure = function() {
+ return SolidusPaypalBraintree.PromiseShim.convertBraintreePromise(braintree.threeDSecure.create, [{
+ client: this._braintreeInstance,
+ version: 2
+ }]).then(function (threeDSecureInstance) {
+ this._threeDSecureInstance = threeDSecureInstance;
+ }.bind(this), function(error) {
+ console.log(error);
+ });
+};
diff --git a/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js b/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js
index a356dbfa..31f5b6a3 100644
--- a/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js
+++ b/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js
@@ -4,7 +4,11 @@ SolidusPaypalBraintree.HostedForm = function(paymentMethodId) {
};
SolidusPaypalBraintree.HostedForm.prototype.initialize = function() {
- this.client = SolidusPaypalBraintree.createClient({paymentMethodId: this.paymentMethodId});
+ this.client = SolidusPaypalBraintree.createClient({
+ paymentMethodId: this.paymentMethodId,
+ useThreeDSecure: (typeof(window.threeDSecureOptions) !== 'undefined'),
+ });
+
return this.client.initialize().
then(this._createHostedFields.bind(this));
};
@@ -15,6 +19,7 @@ SolidusPaypalBraintree.HostedForm.prototype._createHostedFields = function () {
}
var opts = {
+ _solidusClient: this.client,
client: this.client.getBraintreeInstance(),
fields: {
diff --git a/app/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb b/app/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb
new file mode 100644
index 00000000..a7eb616c
--- /dev/null
+++ b/app/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb
@@ -0,0 +1,10 @@
+module SolidusPaypalBraintree
+ module CheckoutControllerDecorator
+
+ def self.prepended(base)
+ base.helper ::SolidusPaypalBraintree::BraintreeCheckoutHelper
+ end
+
+ ::Spree::CheckoutController.prepend(self)
+ end
+end
diff --git a/app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb b/app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb
new file mode 100644
index 00000000..52dd21ab
--- /dev/null
+++ b/app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module SolidusPaypalBraintree
+ module BraintreeCheckoutHelper
+
+ def braintree_3ds_options_for(order)
+ ship_address = order.ship_address
+ bill_address = order.bill_address
+
+ {
+ nonce: nil, # populated after tokenization
+ bin: nil, # populated after tokenization
+ onLookupComplete: nil, # populated after tokenization
+ amount: order.total,
+ email: order.email,
+ billingAddress: {
+ givenName: bill_address.firstname,
+ surname: bill_address.lastname,
+ phoneNumber: bill_address.phone,
+ streetAddress: bill_address.address1,
+ extendedAddress: bill_address.address2,
+ locality: bill_address.city,
+ region: bill_address.state&.name,
+ postalCode: bill_address.zipcode,
+ countryCodeAlpha2: bill_address.country&.iso,
+ },
+ additionalInformation: {
+ shippingGivenName: ship_address.firstname,
+ shippingSurname: ship_address.lastname,
+ shippingPhone: ship_address.phone,
+ shippingAddress: {
+ streedAddress: ship_address.address1,
+ extendedAddress: ship_address.address2,
+ locality: ship_address.city,
+ region: ship_address.state&.name,
+ postalCode: ship_address.zipcode,
+ countryCodeAlpha2: ship_address.country&.iso,
+ }
+ }
+ }
+ end
+
+ end
+end
diff --git a/app/views/spree/shared/_braintree_errors.html.erb b/app/views/spree/shared/_braintree_errors.html.erb
index 690fd07a..791a39c7 100644
--- a/app/views/spree/shared/_braintree_errors.html.erb
+++ b/app/views/spree/shared/_braintree_errors.html.erb
@@ -8,4 +8,6 @@
BraintreeError.HOSTED_FIELDS_FIELD_DUPLICATE_IFRAME = "<%= I18n.t('solidus_paypal_braintree.errors.duplicate_iframe')%>"
BraintreeError.HOSTED_FIELDS_TOKENIZATION_FAIL_ON_DUPLICATE = "<%= I18n.t('solidus_paypal_braintree.errors.fail_on_duplicate')%>"
BraintreeError.HOSTED_FIELDS_TOKENIZATION_CVV_VERIFICATION_FAILED = "<%= I18n.t('solidus_paypal_braintree.errors.cvv_verification_failed')%>"
+ BraintreeError.THREEDS_AUTHENTICATION_FAILED = "<%= t('solidus_paypal_braintree.errors.threeds.authentication_failed') %>";
+ BraintreeError.THREEDS_CARDINAL_SDK_ERROR = "<%= t('solidus_paypal_braintree.errors.threeds.authentication_failed') %>";
diff --git a/app/views/spree/shared/_braintree_hosted_fields.html.erb b/app/views/spree/shared/_braintree_hosted_fields.html.erb
index 275f3250..29c6b10d 100644
--- a/app/views/spree/shared/_braintree_hosted_fields.html.erb
+++ b/app/views/spree/shared/_braintree_hosted_fields.html.erb
@@ -24,3 +24,9 @@
+
+<% if current_store.braintree_configuration.three_d_secure? %>
+
+<% end -%>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 89c1d0ca..c09e052f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -35,6 +35,8 @@ en:
duplicate_iframe: "Duplicate Braintree iframe."
fail_on_duplicate: "This payment method already exists in your vault."
cvv_verification_failed: "CVV did not pass verification."
+ threeds:
+ authentication_failed: "3D Secure authentication failed. Please try again using a different payment method."
payment_type:
label: Payment Type
apple_pay_card: Apple Pay
diff --git a/spec/features/frontend/braintree_credit_card_checkout_spec.rb b/spec/features/frontend/braintree_credit_card_checkout_spec.rb
index 4d87de48..cd9deaa7 100644
--- a/spec/features/frontend/braintree_credit_card_checkout_spec.rb
+++ b/spec/features/frontend/braintree_credit_card_checkout_spec.rb
@@ -4,11 +4,16 @@
shared_context "checkout setup" do
let(:braintree) { new_gateway(active: true) }
let!(:gateway) { create :payment_method }
+ let(:three_d_secure_enabled) { false }
before(:each) do
braintree.save!
+
create(:store, payment_methods: [gateway, braintree]).tap do |store|
- store.braintree_configuration.update!(credit_card: true)
+ store.braintree_configuration.update!(
+ credit_card: true,
+ three_d_secure: three_d_secure_enabled
+ )
end
if SolidusSupport.solidus_gem_version >= Gem::Version.new('2.6.0')
@@ -44,27 +49,59 @@
cassette_name: 'checkout/valid_credit_card',
match_requests_on: [:braintree_uri]
} do
+ let(:card_number) { "4111111111111111" }
+ let(:card_expiration) { "01/#{Time.now.year+2}" }
+ let(:card_cvv) { "123" }
include_context "checkout setup"
- it "checks out successfully" do
+ before do
within_frame("braintree-hosted-field-number") do
- fill_in("credit-card-number", with: "4111111111111111")
+ fill_in("credit-card-number", with: card_number)
end
within_frame("braintree-hosted-field-expirationDate") do
- fill_in("expiration", with: "02/2020")
+ fill_in("expiration", with: card_expiration)
end
within_frame("braintree-hosted-field-cvv") do
- fill_in("cvv", with: "123")
+ fill_in("cvv", with: card_cvv)
end
click_button("Save and Continue")
+ end
+
+ it "checks out successfully" do
within("#order_details") do
expect(page).to have_content("CONFIRM")
end
click_button("Place Order")
expect(page).to have_content("Your order has been processed successfully")
end
+
+ context 'and having 3D secure enabled' do
+ let(:three_d_secure_enabled) { true }
+ let(:card_number) { "4000000000001091" }
+
+ it 'checks out successfully' do
+ authenticate_3ds
+
+ within("#order_details") do
+ expect(page).to have_content("CONFIRM")
+ end
+
+ click_button("Place Order")
+ expect(page).to have_content("Your order has been processed successfully")
+ end
+
+ context 'with 3ds authentication error' do
+ let(:card_number) { "4000000000001125" }
+
+ it 'shows a 3ds authentication error' do
+ authenticate_3ds
+ expect(page).to have_content("3D Secure authentication failed. Please try again using a different payment method.")
+ end
+ end
+
+ end
end
context "with invalid credit card data" do
@@ -113,4 +150,11 @@
end
end
end
+
+ def authenticate_3ds
+ within_frame("Cardinal-CCA-IFrame") do
+ fill_in("challengeDataEntry", with: "1234")
+ click_button("SUBMIT")
+ end
+ end
end