From e0add97e3b49bb3dee2c99ad62416c444442e492 Mon Sep 17 00:00:00 2001 From: Dumitru Ceban Date: Wed, 11 Sep 2019 16:36:24 +0200 Subject: [PATCH 1/4] Bump Braintree hosted scripts version to 3.52.0 The 3.52.0 version is the minimum version required to be able to introduce the 3D Secure 2 support. --- .../shared/_paypal_braintree_head_scripts.html.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb b/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb index 7783e640..3a53182a 100644 --- a/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb +++ b/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb @@ -1,18 +1,18 @@ <% content_for :head do %> - - + + <% if current_store.braintree_configuration.paypal? %> - + <% end %> <% if current_store.braintree_configuration.credit_card? %> - + <% end %> <% if current_store.braintree_configuration.apple_pay? %> - + <% end %> <%= javascript_include_tag "solidus_paypal_braintree/checkout" %> From 26ceb0f299f4ac7e4ae52a829d378dcb1431617b Mon Sep 17 00:00:00 2001 From: Dumitru Ceban Date: Wed, 11 Sep 2019 16:44:16 +0200 Subject: [PATCH 2/4] Introduce 3D Secure configuration flag --- .../_paypal_braintree_head_scripts.html.erb | 4 +++ config/locales/en.yml | 6 ++++ ...dd_3d_secure_to_braintree_configuration.rb | 6 ++++ .../configurations_controller.rb | 1 + .../configurations/list.html.erb | 29 +++++++++++++++---- 5 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb diff --git a/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb b/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb index 3a53182a..d5ae290b 100644 --- a/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb +++ b/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb @@ -9,6 +9,10 @@ <% if current_store.braintree_configuration.credit_card? %> + + <% if current_store.braintree_configuration.three_d_secure? %> + + <% end %> <% end %> <% if current_store.braintree_configuration.apple_pay? %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 74435e0f..89c1d0ca 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,6 +2,12 @@ en: activerecord: models: solidus_paypal_braintree/gateway: Braintree + attributes: + solidus_paypal_braintree/configuration: + paypal: PayPal + apple_pay: Apple Pay + credit_card: Credit Card + three_d_secure: 3D Secure spree: admin: tab: diff --git a/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb b/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb new file mode 100644 index 00000000..21802976 --- /dev/null +++ b/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb @@ -0,0 +1,6 @@ +class Add3dSecureToBraintreeConfiguration < SolidusSupport::Migration[4.2] + def change + add_column :solidus_paypal_braintree_configurations, :three_d_secure, :boolean, null: false, default: false + end +end + diff --git a/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb b/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb index 222a51d2..cd607810 100644 --- a/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +++ b/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb @@ -29,6 +29,7 @@ def configurations_params :paypal, :apple_pay, :credit_card, + :three_d_secure, :preferred_paypal_button_locale, :preferred_paypal_button_color, :preferred_paypal_button_size, diff --git a/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb b/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb index f16f9d11..9899f3a4 100644 --- a/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +++ b/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb @@ -6,20 +6,37 @@ <% @configurations.each do |config| %>
-

<%= config.store.name %>

+ <%= config.store.name %> <%= f.fields_for 'configuration_fields[]', config do |c| %>
- <%= c.label :paypal %> <%= c.check_box :paypal %> + <%= c.label :paypal do %> + <%= tag.i class: 'fa fa-paypal' %> + <%= c.object.class.human_attribute_name(:paypal) %> + <% end %>
+
- <%= c.label :apple_pay %> <%= c.check_box :apple_pay %> + <%= c.label :apple_pay do %> + <%= tag.i class: 'fa fa-apple' %> + <%= c.object.class.human_attribute_name(:apple_pay) %> + <% end %>
+
- <%= c.label :credit_card %> <%= c.check_box :credit_card %> + <%= c.label :credit_card do %> + <%= tag.i class: 'fa fa-credit-card' %> + <%= c.object.class.human_attribute_name(:credit_card) %> + <% end %> + + + <%= c.check_box :three_d_secure %> + <%= c.label :three_d_secure do %> + <%= tag.i class: 'fa fa-shield' %> + <%= c.object.class.human_attribute_name(:three_d_secure) %> + <% end %>
<% config.admin_form_preference_names.each do |name| %> @@ -32,5 +49,7 @@
<% end %> - <%= submit_tag %> +
+ <%= submit_tag "Update", class: 'btn btn-primary' %> +
<% end %> From 28d0de963a5e8bf3f8f539dd8bb30d80717b4154 Mon Sep 17 00:00:00 2001 From: Dumitru Ceban Date: Wed, 11 Sep 2019 16:48:15 +0200 Subject: [PATCH 3/4] Add 3DS authentication on checkout --- .../solidus_paypal_braintree/checkout.js | 26 ++++++++- .../solidus_paypal_braintree/client.js | 23 ++++++-- .../solidus_paypal_braintree/hosted_form.js | 7 ++- .../checkout_controller_decorator.rb | 10 ++++ .../braintree_checkout_helper.rb | 44 +++++++++++++++ .../spree/shared/_braintree_errors.html.erb | 2 + .../shared/_braintree_hosted_fields.html.erb | 6 +++ config/locales/en.yml | 2 + .../braintree_credit_card_checkout_spec.rb | 54 +++++++++++++++++-- 9 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 app/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb create mode 100644 app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb 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 From 91dd085b7a9b78bc2db7d22e5f993cd5ae0ffaf8 Mon Sep 17 00:00:00 2001 From: Dumitru Ceban Date: Fri, 18 Oct 2019 17:53:52 +0200 Subject: [PATCH 4/4] Document how to enable 3D Secure in Solidus --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 23c67886..f985e310 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,19 @@ has each payment type disabled. It also adds a `before_create` callback to default configuration that gets created by overriding the private `build_default_configuration` method on `Spree::Store`. +### 3D Secure + +This gem supports [3D Secure 2](https://developers.braintreepayments.com/guides/3d-secure/overview), +which satisfies the [Strong Customer Authentication (SCA)](https://www.braintreepayments.com/blog/getting-up-to-speed-on-psd2-regulation-2/) +requirements introduced by PSD2. + +3D Secure can be enabled from Solidus Admin -> Braintree (left-side menu) -> +tick _3D Secure_ checkbox. + +Once enabled, you can use the following card numbers to test 3DS 2 on your +client side in sandbox: +https://developers.braintreepayments.com/guides/3d-secure/migration/javascript/v3#client-side-sandbox-testing. + Testing -------