solidus_paypal_braintree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +196 -0
  4. data/Rakefile +30 -0
  5. data/app/assets/javascripts/spree/backend/solidus_paypal_braintree.js +66 -0
  6. data/app/assets/javascripts/spree/braintree_hosted_form.js +98 -0
  7. data/app/assets/javascripts/spree/checkout/braintree.js +60 -0
  8. data/app/assets/javascripts/spree/frontend/paypal_button.js +182 -0
  9. data/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +188 -0
  10. data/app/assets/stylesheets/spree/backend/solidus_paypal_braintree.scss +28 -0
  11. data/app/assets/stylesheets/spree/frontend/solidus_paypal_braintree.css +16 -0
  12. data/app/controllers/solidus_paypal_braintree/client_tokens_controller.rb +21 -0
  13. data/app/helpers/braintree_admin_helper.rb +18 -0
  14. data/app/models/application_record.rb +3 -0
  15. data/app/models/solidus_paypal_braintree/configuration.rb +5 -0
  16. data/app/models/solidus_paypal_braintree/customer.rb +4 -0
  17. data/app/models/solidus_paypal_braintree/gateway.rb +323 -0
  18. data/app/models/solidus_paypal_braintree/response.rb +52 -0
  19. data/app/models/solidus_paypal_braintree/source.rb +73 -0
  20. data/app/models/solidus_paypal_braintree/transaction.rb +30 -0
  21. data/app/models/solidus_paypal_braintree/transaction_address.rb +66 -0
  22. data/app/models/solidus_paypal_braintree/transaction_import.rb +92 -0
  23. data/app/models/spree/store_decorator.rb +11 -0
  24. data/app/overrides/admin_navigation_menu.rb +6 -0
  25. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +26 -0
  26. data/config/initializers/braintree.rb +1 -0
  27. data/config/locales/en.yml +30 -0
  28. data/config/routes.rb +12 -0
  29. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
  30. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +11 -0
  31. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
  32. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +9 -0
  33. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
  34. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +30 -0
  35. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +11 -0
  36. data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +30 -0
  37. data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +27 -0
  38. data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +61 -0
  39. data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +37 -0
  40. data/lib/solidus_paypal_braintree.rb +10 -0
  41. data/lib/solidus_paypal_braintree/country_mapper.rb +35 -0
  42. data/lib/solidus_paypal_braintree/engine.rb +53 -0
  43. data/lib/solidus_paypal_braintree/factories.rb +18 -0
  44. data/lib/solidus_paypal_braintree/version.rb +3 -0
  45. data/lib/views/backend/solidus_paypal_braintree/configurations/_admin_tab.html.erb +3 -0
  46. data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +30 -0
  47. data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
  48. data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +34 -0
  49. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
  50. data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +52 -0
  51. metadata +350 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Constructor for PayPal button object
3
+ * @constructor
4
+ * @param {object} element - The DOM element of your PayPal button
5
+ */
6
+ function PaypalButton(element) {
7
+ this.element = element;
8
+ }
9
+
10
+ /**
11
+ * Creates the PayPal session using the provided options and enables the button
12
+ *
13
+ * @param {object} options - The options passed to tokenize when constructing
14
+ * the PayPal instance
15
+ *
16
+ * See {@link https://braintree.github.io/braintree-web/3.9.0/PayPal.html#tokenize}
17
+ */
18
+ PaypalButton.prototype.initialize = function(options) {
19
+ var self = this;
20
+
21
+ /* This sets the payment method id returned by fetchToken on the PaypalButton
22
+ * instance so that we can use it to build the transaction params later. */
23
+ SolidusPaypalBraintree.fetchToken(function(token, paymentMethodId) {
24
+ self.paymentMethodId = paymentMethodId;
25
+
26
+ SolidusPaypalBraintree.initializeWithDataCollector(token, function(client) {
27
+ self.createPaypalInstance(client, function(paypal) {
28
+
29
+ self.initializePaypalSession({
30
+ paypalInstance: paypal,
31
+ paypalButton: self.element,
32
+ paypalOptions: options
33
+ }, self.tokenizeCallback.bind(self));
34
+ });
35
+
36
+ });
37
+ });
38
+ };
39
+
40
+ PaypalButton.prototype.createPaypalInstance = function(braintreeClient, readyCallback) {
41
+ braintree.paypal.create({
42
+ client: braintreeClient
43
+ }, function (paypalErr, paypalInstance) {
44
+ if (paypalErr) {
45
+ console.error("Error creating PayPal:", paypalErr);
46
+ return;
47
+ }
48
+ readyCallback(paypalInstance);
49
+ });
50
+ };
51
+
52
+ /* Initializes and begins the Paypal session
53
+ *
54
+ * @param config Configuration settings for the session
55
+ * @param config.paypalInstance {object} The Paypal instance returned by Braintree
56
+ * @param config.paypalButton {object} The button DOM element
57
+ * @param config.paypalOptions {object} Configuration options for Paypal
58
+ * @param config.error {tokenizeErrorCallback} Callback function for tokenize errors
59
+ * @param {tokenizeCallback} callback Callback function for tokenization
60
+ */
61
+ PaypalButton.prototype.initializePaypalSession = function(config, callback) {
62
+ config.paypalButton.removeAttribute('disabled');
63
+ config.paypalButton.addEventListener('click', function(event) {
64
+ config.paypalInstance.tokenize(config.paypalOptions, callback);
65
+ }, false);
66
+ },
67
+
68
+ /**
69
+ * Default callback function for when tokenization completes
70
+ *
71
+ * @param {object|null} tokenizeErr - The error returned by Braintree on failure
72
+ * @param {object} payload - The payload returned by Braintree on success
73
+ */
74
+ PaypalButton.prototype.tokenizeCallback = function(tokenizeErr, payload) {
75
+ if (tokenizeErr) {
76
+ console.error('Error tokenizing:', tokenizeErr);
77
+ } else {
78
+ var params = this.transactionParams(payload);
79
+
80
+ Spree.ajax({
81
+ url: Spree.pathFor("solidus_paypal_braintree/transactions"),
82
+ type: 'POST',
83
+ dataType: 'json',
84
+ data: params,
85
+ success: function(response) {
86
+ window.location.href = response.redirectUrl;
87
+ },
88
+ error: function(xhr) {
89
+ console.error("Error submitting transaction")
90
+ },
91
+ });
92
+ }
93
+ };
94
+
95
+ /**
96
+ * Assigns a new callback function for when tokenization completes
97
+ *
98
+ * @callback callback - The callback function to assign
99
+ */
100
+ PaypalButton.prototype.setTokenizeCallback = function(callback) {
101
+ this.tokenizeCallback = callback;
102
+ };
103
+
104
+ /**
105
+ * Builds the transaction parameters to submit to Solidus for the given
106
+ * payload returned by Braintree
107
+ *
108
+ * @param {object} payload - The payload returned by Braintree after tokenization
109
+ */
110
+ PaypalButton.prototype.transactionParams = function(payload) {
111
+ return {
112
+ "payment_method_id" : this.paymentMethodId,
113
+ "transaction" : {
114
+ "email" : payload.details.email,
115
+ "phone" : payload.details.phone,
116
+ "nonce" : payload.nonce,
117
+ "payment_type" : payload.type,
118
+ "address_attributes" : this.addressParams(payload)
119
+ }
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Builds the address parameters to submit to Solidus using the payload
125
+ * returned by Braintree
126
+ *
127
+ * @param {object} payload - The payload returned by Braintree after tokenization
128
+ */
129
+ PaypalButton.prototype.addressParams = function(payload) {
130
+ if (payload.details.shippingAddress.recipientName) {
131
+ var first_name = payload.details.shippingAddress.recipientName.split(" ")[0];
132
+ var last_name = payload.details.shippingAddress.recipientName.split(" ")[1];
133
+ }
134
+ if (first_name == null || last_name == null) {
135
+ var first_name = payload.details.firstName;
136
+ var last_name = payload.details.lastName;
137
+ }
138
+
139
+ return {
140
+ "first_name" : first_name,
141
+ "last_name" : last_name,
142
+ "address_line_1" : payload.details.shippingAddress.line1,
143
+ "address_line_2" : payload.details.shippingAddress.line2,
144
+ "city" : payload.details.shippingAddress.city,
145
+ "state_code" : payload.details.shippingAddress.state,
146
+ "zip" : payload.details.shippingAddress.postalCode,
147
+ "country_code" : payload.details.shippingAddress.countryCode
148
+ }
149
+ };
150
+
151
+ $(document).ready(function() {
152
+ if (document.getElementById("empty-cart")) {
153
+ $.when(
154
+ $.getScript("https://js.braintreegateway.com/web/3.9.0/js/client.min.js"),
155
+ $.getScript("https://js.braintreegateway.com/web/3.9.0/js/paypal.min.js"),
156
+ $.getScript("https://js.braintreegateway.com/web/3.9.0/js/data-collector.min.js"),
157
+ $.Deferred(function( deferred ){
158
+ $( deferred.resolve );
159
+ })
160
+ ).done(function() {
161
+ $('<script/>').attr({
162
+ 'data-merchant' : "braintree",
163
+ 'data-id' : "paypal-button",
164
+ 'data-button' : "checkout",
165
+ 'data-color' : "blue",
166
+ 'data-size' : "medium",
167
+ 'data-shape' : "pill",
168
+ 'data-button_type' : "button",
169
+ 'data-button_disabled' : "true"
170
+ }).
171
+ load(function() {
172
+ var button = new PaypalButton(document.querySelector("#paypal-button"));
173
+ button.initialize({
174
+ flow: 'vault',
175
+ enableShippingAddress: true,
176
+ });
177
+ }).
178
+ insertAfter("#content").
179
+ attr('src', 'https://www.paypalobjects.com/api/button.js?')
180
+ });
181
+ }
182
+ });
@@ -0,0 +1,188 @@
1
+ //= require spree/braintree_hosted_form
2
+
3
+ window.SolidusPaypalBraintree = {
4
+ APPLE_PAY_API_VERSION: 1,
5
+
6
+ // Override to provide your own error messages.
7
+ braintreeErrorHandle: function(braintreeError) {
8
+ var $contentContainer = $("#content");
9
+ var $flash = $("<div class='flash error'>" + braintreeError.name + ": " + braintreeError.message + "</div>");
10
+ $contentContainer.prepend($flash);
11
+
12
+ $flash.show().delay(5000).fadeOut(500);
13
+ },
14
+
15
+ fetchToken: function(tokenCallback) {
16
+ Spree.ajax({
17
+ dataType: 'json',
18
+ type: 'POST',
19
+ url: Spree.pathFor('solidus_paypal_braintree/client_token'),
20
+ success: function(response) {
21
+ tokenCallback(response.client_token, response.payment_method_id);
22
+ },
23
+ error: function(xhr) {
24
+ console.error("Error fetching braintree token");
25
+ }
26
+ });
27
+ },
28
+
29
+ initialize: function(authToken, clientReadyCallback) {
30
+ braintree.client.create({
31
+ authorization: authToken
32
+ }, function (clientErr, clientInstance) {
33
+ if (clientErr) {
34
+ console.error('Error creating client:', clientErr);
35
+ return;
36
+ }
37
+ clientReadyCallback(clientInstance);
38
+ });
39
+ },
40
+
41
+ initializeWithDataCollector: function(authToken, clientReadyCallback) {
42
+ braintree.client.create({
43
+ authorization: authToken
44
+ }, function (clientErr, clientInstance) {
45
+ braintree.dataCollector.create({
46
+ client: clientInstance,
47
+ paypal: true
48
+ }, function (err, dataCollectorInstance) {
49
+ if (err) {
50
+ console.error('Error creating data collector:', err);
51
+ return;
52
+ }
53
+ });
54
+ if (clientErr) {
55
+ console.error('Error creating client:', clientErr);
56
+ return;
57
+ }
58
+ clientReadyCallback(clientInstance);
59
+ });
60
+ },
61
+
62
+ setupApplePay: function(braintreeClient, merchantId, readyCallback) {
63
+ if(window.ApplePaySession && location.protocol == "https:") {
64
+ var promise = ApplePaySession.canMakePaymentsWithActiveCard(merchantId);
65
+ promise.then(function (canMakePayments) {
66
+ if (canMakePayments) {
67
+ braintree.applePay.create({
68
+ client: braintreeClient
69
+ }, function (applePayErr, applePayInstance) {
70
+ if (applePayErr) {
71
+ console.error("Error creating ApplePay:", applePayErr);
72
+ return;
73
+ }
74
+ readyCallback(applePayInstance);
75
+ });
76
+ }
77
+ });
78
+ };
79
+ },
80
+
81
+ /* Initializes and begins the ApplePay session
82
+ *
83
+ * @param config Configuration settings for the session
84
+ * @param config.applePayInstance {object} The instance returned from applePay.create
85
+ * @param config.storeName {String} The name of the store
86
+ * @param config.paymentRequest {object} The payment request to submit
87
+ * @param config.currentUserEmail {String|undefined} The active user's email
88
+ * @param config.paymentMethodId {Integer} The SolidusPaypalBraintree::Gateway id
89
+ */
90
+ initializeApplePaySession: function(config, sessionCallback) {
91
+
92
+ var requiredFields = ['postalAddress', 'phone'];
93
+
94
+ if (!config.currentUserEmail) {
95
+ requiredFields.push('email');
96
+ }
97
+
98
+ config.paymentRequest['requiredShippingContactFields'] = requiredFields
99
+ var paymentRequest = config.applePayInstance.createPaymentRequest(config.paymentRequest);
100
+
101
+ var session = new ApplePaySession(SolidusPaypalBraintree.APPLE_PAY_API_VERSION, paymentRequest);
102
+ session.onvalidatemerchant = function (event) {
103
+ config.applePayInstance.performValidation({
104
+ validationURL: event.validationURL,
105
+ displayName: config.storeName,
106
+ }, function (validationErr, merchantSession) {
107
+ if (validationErr) {
108
+ console.error('Error validating Apple Pay:', validationErr);
109
+ session.abort();
110
+ return;
111
+ };
112
+ session.completeMerchantValidation(merchantSession);
113
+ });
114
+ };
115
+
116
+ session.onpaymentauthorized = function (event) {
117
+ config.applePayInstance.tokenize({
118
+ token: event.payment.token
119
+ }, function (tokenizeErr, payload) {
120
+ if (tokenizeErr) {
121
+ console.error('Error tokenizing Apple Pay:', tokenizeErr);
122
+ session.completePayment(ApplePaySession.STATUS_FAILURE);
123
+ }
124
+
125
+ var contact = event.payment.shippingContact;
126
+
127
+ Spree.ajax({
128
+ data: SolidusPaypalBraintree.buildTransaction(payload, config, contact),
129
+ dataType: 'json',
130
+ type: 'POST',
131
+ url: Spree.pathFor('solidus_paypal_braintree/transactions'),
132
+ success: function(response) {
133
+ session.completePayment(ApplePaySession.STATUS_SUCCESS);
134
+ window.location.replace(response.redirectUrl);
135
+ },
136
+ error: function(xhr) {
137
+ if (xhr.status === 422) {
138
+ var errors = xhr.responseJSON.errors
139
+
140
+ if (errors && errors["Address"]) {
141
+ session.completePayment(ApplePaySession.STATUS_INVALID_SHIPPING_POSTAL_ADDRESS);
142
+ } else {
143
+ session.completePayment(ApplePaySession.STATUS_FAILURE);
144
+ }
145
+ }
146
+ }
147
+ });
148
+
149
+ });
150
+ };
151
+
152
+ sessionCallback(session);
153
+
154
+ session.begin();
155
+ },
156
+
157
+ buildTransaction: function(payload, config, shippingContact) {
158
+ return {
159
+ transaction: {
160
+ nonce: payload.nonce,
161
+ phone: shippingContact.phoneNumber,
162
+ email: config.currentUserEmail || shippingContact.emailAddress,
163
+ payment_type: payload.type,
164
+ address_attributes: SolidusPaypalBraintree.buildAddress(shippingContact)
165
+ },
166
+ payment_method_id: config.paymentMethodId
167
+ };
168
+ },
169
+
170
+ buildAddress: function(shippingContact) {
171
+ var addressHash = {
172
+ country_name: shippingContact.country,
173
+ country_code: shippingContact.countryCode,
174
+ first_name: shippingContact.givenName,
175
+ last_name: shippingContact.familyName,
176
+ state_code: shippingContact.administrativeArea,
177
+ city: shippingContact.locality,
178
+ zip: shippingContact.postalCode,
179
+ address_line_1: shippingContact.addressLines[0]
180
+ };
181
+
182
+ if(shippingContact.addressLines.length > 1) {
183
+ addressHash['address_line_2'] = shippingContact.addressLines[1];
184
+ }
185
+
186
+ return addressHash;
187
+ }
188
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4
+ */
5
+
6
+ // Set Bootstrap variables if they're not already defined
7
+ // e.g if we're on Solidus < v1.4
8
+ $input-border-color: #cee1f4 !default;
9
+ $border-radius: 3px !default;
10
+ $input-color: #5498DA !default;
11
+ $brand-danger: #C60F13 !default;
12
+ $brand-success: #9FC820 !default;
13
+
14
+ .braintree-hosted-fields .input {
15
+ border: 1px solid $input-border-color;
16
+ border-radius: $border-radius;
17
+ color: $input-color;
18
+ height: 30px;
19
+ padding: 7px 10px;
20
+
21
+ &.braintree-hosted-fields-invalid {
22
+ border-color: $brand-danger;
23
+ }
24
+
25
+ &.braintree-hosted-fields-valid {
26
+ border-color: $brand-success;
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
5
+
6
+ .braintree-hosted-fields .input {
7
+ border: 1px solid #d9d9db;
8
+ border-radius: 3px;
9
+ color: #5498DA;
10
+ height: 30px;
11
+ padding: 5px 10px;
12
+ }
13
+
14
+ .paypal-button-widget .paypal-button:hover {
15
+ background: transparent;
16
+ }
@@ -0,0 +1,21 @@
1
+ module SolidusPaypalBraintree
2
+ class ClientTokensController < Spree::Api::BaseController
3
+ skip_before_action :authenticate_user
4
+
5
+ before_action :load_gateway
6
+
7
+ def create
8
+ render json: { client_token: @gateway.generate_token, payment_method_id: @gateway.id }
9
+ end
10
+
11
+ private
12
+
13
+ def load_gateway
14
+ if params[:payment_method_id]
15
+ @gateway = ::SolidusPaypalBraintree::Gateway.find_by!(id: params[:payment_method_id])
16
+ else
17
+ @gateway = ::SolidusPaypalBraintree::Gateway.find_by!(active: true)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ module BraintreeAdminHelper
2
+ # Returns a link to the Braintree web UI for the given Braintree payment
3
+ def braintree_transaction_link(payment)
4
+ environment = payment.payment_method.preferred_environment == 'sandbox' ? 'sandbox' : 'www'
5
+ merchant_id = payment.payment_method.preferred_merchant_id
6
+ response_code = payment.response_code
7
+
8
+ return unless response_code.present?
9
+ return response_code unless merchant_id.present?
10
+
11
+ link_to(
12
+ response_code,
13
+ "https://#{environment}.braintreegateway.com/merchants/#{merchant_id}/transactions/#{response_code}",
14
+ title: 'Show payment on Braintree',
15
+ target: '_blank'
16
+ )
17
+ end
18
+ end