Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #232 from nebulab/3ds-support
Browse files Browse the repository at this point in the history
Introduce 3D Secure support for credit cards
  • Loading branch information
kennyadsl authored Oct 26, 2019
2 parents c21f870 + 91dd085 commit e85a085
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 21 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------

Expand Down
26 changes: 24 additions & 2 deletions app/assets/javascripts/solidus_paypal_braintree/checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
});
});
}
}
Expand Down
23 changes: 20 additions & 3 deletions app/assets/javascripts/solidus_paypal_braintree/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

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

Expand Down Expand Up @@ -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);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
Expand All @@ -15,6 +19,7 @@ SolidusPaypalBraintree.HostedForm.prototype._createHostedFields = function () {
}

var opts = {
_solidusClient: this.client,
client: this.client.getBraintreeInstance(),

fields: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module SolidusPaypalBraintree
module CheckoutControllerDecorator

def self.prepended(base)
base.helper ::SolidusPaypalBraintree::BraintreeCheckoutHelper
end

::Spree::CheckoutController.prepend(self)
end
end
44 changes: 44 additions & 0 deletions app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions app/views/spree/shared/_braintree_errors.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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') %>";
</script>
6 changes: 6 additions & 0 deletions app/views/spree/shared/_braintree_hosted_fields.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@
<input type="hidden" name="<%= prefix %>[payment_type]" value="<%= SolidusPaypalBraintree::Source::CREDIT_CARD %>">
<input type="hidden" id="payment_method_nonce" name="<%= prefix %>[nonce]">
</div>

<% if current_store.braintree_configuration.three_d_secure? %>
<script>
var threeDSecureOptions = <%=raw braintree_3ds_options_for(current_order).to_json %>;
</script>
<% end -%>
14 changes: 9 additions & 5 deletions app/views/spree/shared/_paypal_braintree_head_scripts.html.erb
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
<% content_for :head do %>
<script src="https://js.braintreegateway.com/web/3.34.0/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.34.0/js/data-collector.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.52.0/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.52.0/js/data-collector.min.js"></script>

<% if current_store.braintree_configuration.paypal? %>
<script src="https://js.braintreegateway.com/web/3.34.0/js/paypal-checkout.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.52.0/js/paypal-checkout.min.js"></script>
<script src="https://www.paypalobjects.com/api/checkout.js" data-version-4></script>
<% end %>

<% if current_store.braintree_configuration.credit_card? %>
<script src="https://js.braintreegateway.com/web/3.34.0/js/hosted-fields.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.52.0/js/hosted-fields.min.js"></script>

<% if current_store.braintree_configuration.three_d_secure? %>
<script src="https://js.braintreegateway.com/web/3.52.0/js/three-d-secure.min.js"></script>
<% end %>
<% end %>

<% if current_store.braintree_configuration.apple_pay? %>
<script src="https://js.braintreegateway.com/web/3.34.0/js/apple-pay.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.52.0/js/apple-pay.min.js"></script>
<% end %>

<%= javascript_include_tag "solidus_paypal_braintree/checkout" %>
Expand Down
8 changes: 8 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -29,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,37 @@
<% @configurations.each do |config| %>
<div class="row">
<fieldset>
<h1><%= config.store.name %></h1>
<legend><%= config.store.name %></legend>

<%= f.fields_for 'configuration_fields[]', config do |c| %>
<div class="field">
<%= 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 %>
</div>

<div class="field">
<%= 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 %>
</div>

<div class="field">
<%= 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 %>
</div>

<% config.admin_form_preference_names.each do |name| %>
Expand All @@ -32,5 +49,7 @@
</div>
<% end %>

<%= submit_tag %>
<div class="form-buttons filter-actions actions">
<%= submit_tag "Update", class: 'btn btn-primary' %>
</div>
<% end %>
Loading

0 comments on commit e85a085

Please sign in to comment.