solidus_paypal_braintree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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