From 4d24b325a1caba9b39e4cac5063124a4155b1589 Mon Sep 17 00:00:00 2001 From: Graham Bouvier Date: Fri, 28 Oct 2016 14:17:23 -0700 Subject: [PATCH 01/16] Add javascript functions to initialize and tokenize paypal payments --- .../frontend/solidus_paypal_braintree.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js index 40e58b6d..16c07a47 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js @@ -30,6 +30,54 @@ window.SolidusPaypalBraintree = { }); }, + initializeWithDataCollector: function(authToken, clientReadyCallback) { + braintree.client.create({ + authorization: authToken + }, function (clientErr, clientInstance) { + braintree.dataCollector.create({ + client: clientInstance, + paypal: true + }, function (err, dataCollectorInstance) { + if (err) { + console.error('Error creating data collector:', err); + return; + } + }); + if (clientErr) { + console.error('Error creating client:', clientErr); + return; + } + clientReadyCallback(clientInstance); + }); + }, + + setupPaypal: function(braintreeClient, readyCallback) { + braintree.paypal.create({ + client: braintreeClient + }, function (paypalErr, paypalInstance) { + if (paypalErr) { + console.error("Error creating PayPal:", paypalErr); + return; + } + readyCallback(paypalInstance); + }); + }, + + initializePaypalSession: function(paypalInstance, paypalButton, paypalOptions, submitCallback) { + paypalButton.removeAttribute('disabled'); + paypalButton.addEventListener('click', function(event) { + paypalInstance.tokenize(paypalOptions, function(tokenizeErr, payload) { + if (tokenizeErr) { + if (tokenizeErr.type !== 'CUSTOMER') { + console.error('Error tokenizing:', tokenizeErr); + } + return; + } + submitCallback(payload); + }); + }, false); + }, + setupApplePay: function(braintreeClient, merchantId, readyCallback) { if(window.ApplePaySession) { var promise = ApplePaySession.canMakePaymentsWithActiveCard(merchantId); From 12713b2233bdea27cac90eef477e9957bb11bf39 Mon Sep 17 00:00:00 2001 From: Graham Bouvier Date: Fri, 28 Oct 2016 14:20:00 -0700 Subject: [PATCH 02/16] Add functions create and submit a paypal order These functions will dynamically add a paypal button, fetch a client token, and submit the transaction to solidus upon completion. --- .../solidus_paypal_braintree_frontend.js | 91 +++++++++++++++++++ .../install/install_generator.rb | 1 + lib/solidus_paypal_braintree/engine.rb | 5 +- 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js new file mode 100644 index 00000000..28268581 --- /dev/null +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -0,0 +1,91 @@ +initializePaypal = function(authToken, paymentMethodId) { + window.paymentMethodId = paymentMethodId; + window.SolidusPaypalBraintree.initializeWithDataCollector(authToken, function(clientInstance) { + window.SolidusPaypalBraintree.setupPaypal(clientInstance, function(paypalInstance) { + var paypalButton = document.querySelector('#paypal-button'); + if (document.querySelector('#shipping_address')) { + var address = JSON.parse(document.querySelector('#shipping_address').value); + var paypalOptions = {flow: 'vault', shippingAddressOverride: address, shippingAddressEditable: false, enableShippingAddress: true} + } else { + var paypalOptions = {flow: 'vault', enableShippingAddress: true} + } + window.SolidusPaypalBraintree.initializePaypalSession(paypalInstance, paypalButton, paypalOptions, submitBraintreeTransaction); + }); + }); +}; + +initializePaypalCredit = function(authToken, paymentMethodId) { + window.paymentMethodId = paymentMethodId; + window.SolidusPaypalBraintree.initializeWithDataCollector(authToken, function(clientInstance) { + window.SolidusPaypalBraintree.setupPaypal(clientInstance, function(paypalInstance) { + var paypalButton = document.querySelector('#paypal-credit-button'); + var address = JSON.parse(document.querySelector('#shipping_address').value); + var paypalOptions = {flow: 'checkout', amount: amount, currency: currency, shippingAddressOverride: address, shippingAddressEditable: false, enableShippingAddress: true} + window.SolidusPaypalBraintree.initializePaypalSession(paypalInstance, paypalButton, paypalOptions, submitBraintreeTransaction); + }); + }); +}; + +submitBraintreeTransaction = function(payload) { + if (payload.details.shippingAddress.recipientName) { + var first_name = payload.details.shippingAddress.recipientName.split(" ")[0]; + var last_name = payload.details.shippingAddress.recipientName.split(" ")[1]; + } + if (first_name == null || last_name == null) { + var first_name = payload.details.firstName; + var last_name = payload.details.lastName; + } + var transactionParams = { "payment_method_id" : window.paymentMethodId, + "transaction" : { "email" : payload.details.email, + "phone" : payload.details.phone, + "nonce" : payload.nonce, + "payment_type" : payload.type, + "address_attributes" : { "first_name" : first_name, + "last_name" : last_name, + "address_line_1" : payload.details.shippingAddress.line1, + "address_line_2" : payload.details.shippingAddress.line2, + "city" : payload.details.shippingAddress.city, + "state_code" : payload.details.shippingAddress.state, + "zip" : payload.details.shippingAddress.postalCode, + "country_code" : payload.details.shippingAddress.countryCode } } } + Spree.ajax({ + url: Spree.pathFor("solidus_paypal_braintree/transactions"), + type: 'POST', + dataType: 'json', + data: transactionParams, + success: function(response) { + window.location.href = Spree.pathFor("checkout/confirm"); + }, + error: function(xhr) { + console.error("Error submitting transaction") + }, + }); +}; + +$(document).ready(function() { + if (document.getElementById("empty-cart")) { + $.when( + $.getScript("https://js.braintreegateway.com/web/3.4.0/js/client.min.js"), + $.getScript("https://js.braintreegateway.com/web/3.4.0/js/paypal.min.js"), + $.getScript("https://js.braintreegateway.com/web/3.4.0/js/data-collector.min.js"), + $.Deferred(function( deferred ){ + $( deferred.resolve ); + }) + ).done(function() { + $(window).load(function() { + $(' + + + + + + diff --git a/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb b/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb new file mode 100644 index 00000000..05971b2a --- /dev/null +++ b/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb new file mode 100644 index 00000000..e7d43536 --- /dev/null +++ b/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb @@ -0,0 +1,7 @@ +<% payment_method = current_store.payment_methods.where(active: true).find_by_type('SolidusPaypalBraintree::Gateway') %> +<% if payment_method %> + <% if current_order.valid? %> + <%= render partial: 'solidus_paypal_braintree/address_field', locals: { shipping_address: current_order.shipping_address } %> + <% end %> + <%= render partial: 'solidus_paypal_braintree/paypal_button' %> +<% end %> diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb new file mode 100644 index 00000000..bf4da10b --- /dev/null +++ b/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb @@ -0,0 +1,9 @@ +<% payment_method = current_store.payment_methods.where(active: true).find_by_type('SolidusPaypalBraintree::Gateway') %> +<% if payment_method %> + <%= render partial: 'solidus_paypal_braintree/address_field', locals: { shipping_address: current_order.shipping_address } %> + + <%= render partial: 'solidus_paypal_braintree/paypal_credit_button' %> +<% end %> From 9dade57c673a130428bd0168d724217d2baeaf5f Mon Sep 17 00:00:00 2001 From: Graham Bouvier Date: Fri, 28 Oct 2016 14:28:25 -0700 Subject: [PATCH 04/16] Add feature specs and cassettes for paypal checkouts --- spec/features/paypal_checkout_spec.rb | 93 +++++ spec/fixtures/cassettes/paypal/checkout.yml | 369 ++++++++++++++++++ .../cassettes/paypal/one_touch_checkout.yml | 277 +++++++++++++ spec/spec_helper.rb | 8 + 4 files changed, 747 insertions(+) create mode 100644 spec/features/paypal_checkout_spec.rb create mode 100644 spec/fixtures/cassettes/paypal/checkout.yml create mode 100644 spec/fixtures/cassettes/paypal/one_touch_checkout.yml diff --git a/spec/features/paypal_checkout_spec.rb b/spec/features/paypal_checkout_spec.rb new file mode 100644 index 00000000..1c1d822f --- /dev/null +++ b/spec/features/paypal_checkout_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe "Checkout", type: :feature, js: true do + Capybara.default_max_wait_time = 30 + let!(:store) { create(:store, payment_methods: [payment_method]) } + let!(:country) { create(:country, states_required: true) } + let!(:state) { create(:state, country: country, abbr: "CA", name: "California") } + let!(:shipping_method) { create(:shipping_method) } + let!(:stock_location) { create(:stock_location) } + let!(:mug) { create(:product, name: "RoR Mug") } + let!(:payment_method) { create_gateway } + let!(:zone) { create(:zone) } + + context "goes through checkout using paypal one touch", vcr: { cassette_name: 'paypal/one_touch_checkout', match_requests_on: [:method, :uri] } do + before do + payment_method + add_mug_to_cart + end + + it "should check out successfully using one touch" do + move_through_paypal_popup + expect(page).to have_content("Shipments") + click_on "Place Order" + expect(page).to have_content("Your order has been processed successfully") + end + end + + context "goes through checkout using paypal", vcr: { cassette_name: 'paypal/checkout', match_requests_on: [:method, :uri] } do + before do + payment_method + add_mug_to_cart + end + + it "should check out successfully through regular checkout" do + expect(page).to have_button("paypal-button") + click_button("Checkout") + fill_in("order_email", with: "stembolt_buyer@stembolttest.com") + click_button("Continue") + expect(page).to have_content("Customer E-Mail") + fill_in_address + click_button("Save and Continue") + expect(page).to have_content("SHIPPING METHOD") + click_button("Save and Continue") + move_through_paypal_popup + expect(page).to have_content("Shipments") + click_on "Place Order" + expect(page).to have_content("Your order has been processed successfully") + end + end + + # Selenium does not clear cookies properly between test runs, even when + # using Capybara.reset_sessions!, see: + # https://github.com/jnicklas/capybara/issues/535 + # + # This causes Paypal to remain logged in and not prompt for an email on the + # second test run and causes the test to fail. Adding conditional logic for + # this greatly increases the test time, so it is left out since CI runs + # these with poltergeist. + def move_through_paypal_popup + expect(page).to have_button("paypal-button") + popup = page.window_opened_by do + click_button("paypal-button") + end + page.switch_to_window(popup) + expect(page).to_not have_selector('body.loading') + page.within_frame("injectedUl") do + fill_in("email", with: "stembolt_buyer@stembolttest.com") + fill_in("password", with: "test1234") + click_button("btnLogin") + end + expect(page).to have_button('Agree & Continue') + click_button("Agree & Continue") + page.switch_to_window(page.windows.first) + end + + def fill_in_address + address = "order_bill_address_attributes" + fill_in "#{address}_firstname", with: "Ryan" + fill_in "#{address}_lastname", with: "Bigg" + fill_in "#{address}_address1", with: "143 Swan Street" + fill_in "#{address}_city", with: "San Jose" + select "United States of America", from: "#{address}_country_id" + select "California", from: "#{address}_state_id" + fill_in "#{address}_zipcode", with: "95131" + fill_in "#{address}_phone", with: "(555) 555-0111" + end + + def add_mug_to_cart + visit spree.root_path + click_link mug.name + click_button "add-to-cart-button" + end +end diff --git a/spec/fixtures/cassettes/paypal/checkout.yml b/spec/fixtures/cassettes/paypal/checkout.yml new file mode 100644 index 00000000..2da24ffb --- /dev/null +++ b/spec/fixtures/cassettes/paypal/checkout.yml @@ -0,0 +1,369 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/client_token + body: + encoding: UTF-8 + string: | + + + 2 + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:32 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"66427b04cf5c730187abd03b1b88c689" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - ecfaf951-ed60-4da1-85a6-36d4e8433bdb + X-Runtime: + - '0.110858' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIABCVE1gAA6RWX3ObOBB/76fI5L1XECVXZtJ0GhswGoNrYgToDSRSAxL4 + gg2GT38r3EuTm1ynM/fgycSsdle/f/j2y1mKq7546sq2+Xyt/6FdXxUNa3nZ + fP98He2c95+uv9y9u2WiLJrj+2NbF83du6ur2z4Tp+KuGDGiCZ6y2Dp5VTuu + F3jPk7DNDXwopKOp70MpThSRka3wIW+25ab0zv4khD99N9PJR3Tp1MFUG8EU + mTSOdH9iUyA9Y7MUIp1ETWMq/Co1g6rW/ThCVEZaiqIhnUgVVNDHtTXV49EN + Rho7Go3DxzTZWn719RyU2tlfaOPG2Z43y/bsV+3klx+Rv7Th83XcLL0n+Dv4 + zllX/wdSF0wGbRqbWoJE/W3HR+oy05etkUrNoLLTs2XA+A7OVHTgMe6yOHjM + EDG/xbqRyeNTseIVW/pjio5tLtMSMKlyZMos5oTJAe6PW74KBza1/Ro5Q/Zg + TjCvTqX1cS3xmMbixFdY0JjvuUuMNKlPKbKOm2qr+aN1BLyrzHWgxu8DA9d0 + qses4mW+4zKbSAs1Ve4KkTcKh/vD2kjPaxT0uaQHapAxTcJDjj7Oe0GfLneJ + 4mfyqkOePAwljU2UJRjqdXHhCz/v55VDmaLzAWq0LfQmCe6A9zJbhRpb+Tfr + 0doztz4x5Jyoi/tiYZZMOsB7CNyQBmYLjhxzLWGfXasFS7/PY9gJ7fdQM62n + t/D2+39mJjNeXudJZ2KIaEwnY77wbjy51/jqftqUn/o0CSaaQK9fYDnfIyFa + 5rx1Hr5fmAjmHfPx97iZ+8XmPl8J0HygsPzTa97o/eMe69g5pfHZ5K6o2G/O + +A9sykfgjLt7ha8dIVLxBIsQdknlWVCYD710CjXMdUz4dL96Nu8twwMz7rs0 + EZs01oXiNzJCkcegidX2oueLvrDq4dXOniHaBzUuaRXcELAtEdiN7IOz2Qkz + tjHaNq1Ol/snYn8ymA361MGrjjPwHV7tbItEFR6ZI7RdhPfhzvlWuEG1sb0x + RGHP7fAvOuFNthSBwlnlCJdOVZCX+H5V2hOF6xyZexazvh6sgS1AKw0BPeB7 + aihMlabDVz5UvNAE9Jfcd/TBBK9qPXetaeYUdEbJ/9HYpe4Nn4D3SJUhS+cL + cwCshzQens9RVwCfgbZ9MT9vSJcvlM7OXW5wT+0OfGmsIQI4gXvRA5PWCfx0 + 4rbZ7yTROLLGbPRuqHQ6hiLwDTyTYoQ8Oiodqp1ySFTg80QfZqzgOZxzSb17 + 9iVkgKSg7UDNeenn5SsdSH3PQCeQn5CJ0Ae0qeoz2DeLzWYbc3Vmnss0AvrH + HY2hN/TisdrtZ/Zu46DKDXUPUasd2b/0BxqHXmZVRGLKtaCnbqT4JJG2nf0A + OB5yiUGXxMmlU0LGQZ+DBOwmpfcXmXPZvXrbWzDrRw6o+1mSlnP2DEpjEXjm + 4vNAg1zT2Vyjg8e7i0fU/itsbpFVA7Y3niC7WROQwQx4SA2xzFE4+4uQwFbP + Xu0FuQz5AO8NMs48/eTjZb4CnpBXgO8rLcI7BTg8QqZ0oK8D7A/cBjpb3fes + CQW1TcFXvGfyOOe+p7SDbLX3ETSrqXdMmuD6kmu6KBaYPyba59sPl/f+u9sP + r38R/A0AAP//AwDvfcy0SAgAAA== + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:38 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/client_token + body: + encoding: UTF-8 + string: | + + + 2 + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:34 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"794372140cdef51e5810697f3455f5f0" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 420c3010-0c34-426f-92ef-b65bbc8ecd32 + X-Runtime: + - '0.097862' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIABKVE1gAA6RW0XKbOBR971dk8t4tiJCtZ9J2EhswGiPXBAvQG0ikBiTw + Bhsbvn6vcLdNdrKdzuxDJjNwde/ROecefPflrORVXzx3Zdt8ujb/MK6vioa3 + omy+fbreRu77j9dfPr+747IsmsP7Q1sXzed3V1d3fSaPxediwIgleMzi2dGv + 2mE1xzuRhG1u4X2hXEM/D5U8MkQHvsT7vNmU69I/BSNTQcwUU1SxRXBOI26Q + 0R9StbFJJCqy+AY1jp1WKYK6OhgfoG5rpsiFJ0SxqkYs2pxJRSSpwpJE9+aT + RwYWuwaLw6c02cyC6v5MSuMczI1h7W7O60V7Dqp2JPMbFESOESz4zXrx7TlY + 3J8C92zC/4EoU3JF2jS2jQTJ+mskBuZxO1CtlSrDYqozswXhIoIzFTuJGHdZ + TJ4yRO2vsWll6vBcLEXFF8GQokObq7QETqoc2SqLBeXqBPfHrViGJz62/Qq5 + p+zRHmFenarZzUrhIY3lUSyxZLHYCY9aaVIfUzQ7rKuNEQyzA/BdZZ4LNUFP + LFyzsR6ySpR5JFQ20hZqqtyTMm80Dw/7lZWeV4j0uWJ7ZtEhTcJ9jm4mXNCn + yz2q9Rn9ap8nj6eSxTbKEgz1przohX/g88tTmaLzHmqMDfSmCe5A9zJbhgZf + BrerYbbjXn3kyD0yD/fF3C65ckH3ELShDcyWArn2SgGeqDXIIujzGDCh3Q5q + xtX4Ft9B/8/MZOLL73zljhxRg5t0yOf+ra92hlg+jOvyY58mZGQJ9PoFl9M9 + Empk7lvn4fncRjDvkA+/p83UL7Z3+VKC54nm8k+/eaP393usYveYxmdbeLLi + vznjP7gpn0Az4e00v84W0UokWIaAJVVnyWA+9DIZ1HDPteGv+9W7CbcK99x6 + 6NJErtPYlFrfrRXKPAZPLDcXP1/8hXUPv3Z3HLGe1LhkFbmlUqZUYm/r7N11 + JO3YwWjTtCZb7J6p89HiDvjThF113ZOI8DJyZnRb4YG70oi2eBdG7tfCI9Xa + 8YcQhb1wwr/YiNfZQhLNs84RodyqoC/5vdfek4XnHrh3lpO/HmcnPgevNBT8 + gB+YpTnVng5f7aHWhSXgv+ShY4827KrRC282TpqCzxj9Px671L2xJ7B7tMrQ + zBRz+wRcn9L49OMc8yToSYzNi/l5Q7t8rn127nJL+Bo76GXwhkrQBO7F9lzN + jrBPR+HYfaSoIdBsyAb/lim342gLewPvlBwgjw7ahxpTjpgCPY/sceIK3sM5 + j9bRj72EDFAMvE30nJf7vHjlA2XuOPgE8hMyEfqAN3V9Bniz2G42sdBnprnc + oOB/3LEYekMvEWtsP7N3E5Mqt/Q9ZK0x8n/5DzwOveyq2MoxN0jPvK3Wk26N + zbQPwOM+Vxh8Sd1cuSVkHPTZK+Bu1H5/kTkX7NXbuwWzvueAvt9MsXLKnpP2 + 2BZ25rLnxIBcM/lUY8KOd5cd0fiX2N6gWQ3c3vqSRpMnIIM56JBacpGjcNov + Somj373CBbkM+QDfDTpMOv3U42W+Ap+QV8DvKy/CNwU0PECmdOCvPeAHbYnJ + lw89b0LJHFuKpei5Oky572vvIEfjPoBnDf2NSRNcX3LNlMUci6fE+HT34fLd + f3f34fUvgr8BAAD//wMAJ+j4AEgIAAA= + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:40 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + 09fe9af0-43c0-4dbc-977c-e1e7a3f1afcb + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:48 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"dadf50459ab398759e3bd003f35a3315" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 1ce26846-2d99-4130-9af5-846985f51651 + X-Runtime: + - '0.312284' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIACCVE1gAA6RTPZObMBDt/SsYelkGf54H40yKNMmkyaVJ41nQgnWnD0YS + Z/PvI8DYd8YpMlfue293pbe7yf4sRfCGxnKtdmE0nYUBqlwzrspd+Pv5G9mE + +3SS5LV1WqJJJ0GQcJbG8XKx2qzWCfVBi3kuP4JyxMdrw8qn+GWdyXXxujzO + E/qebdUFN9YRBRIDxcUudKbGkHaUgH8xuZYVqGaEowQuRmh11Gpco4DzCDth + Zrl70M8gOGQEXOCaCnch86HjEsM0nkUrEs1IvHmONtso3i7WfxJ6S+jy64r9 + X/4toe/feU4KjoLZ65MYdyQHw+ylKBgDzfBnaCoQBPJc18p9VLSCkaQHPZxx + IfzICZQGUWI/qa9k8etHHMXL6Pt6uVnM59HPhD5UDmU+aVlf47Jq5MOavUcH + JcMCajG0yrQWCCpM2xEm9EJexd2WpNahzLRwh6xu0HwZQofWTf1+JbSXDUlc + QomkNiI9OlfZLaVgLTo7zQxw5bwDpX/+CZo2mXpvW0MOEt1Rs4PQpaa939NK + lXtUb9xo1Up2FhTL9Nmfz7XD0NPWmc0Nr5y/yAdD7jROv6JKlyf29MIS2kcD + 98m16/9tSXuvCgXhijve8vc2FyCs9/mR9FrH/74dmSr0/XV1tOCSt51ri0Qb + 1k13LEzoeGnvMduBwJhBP6CRbbf9SSd/AQAA//8DAL1TiRDzBAAA + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:53 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/transactions + body: + encoding: UTF-8 + string: | + + + 29.99 + + true + + R765286042-AKGE5U5C + 5wd9jd + 22546867 + sale + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:53 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"a8562bdb10033faa02696ca2a2bc00b2" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - a582458e-29af-41c6-a02e-a66c83d1c052 + X-Runtime: + - '4.642481' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + 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 + AAA= + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:58 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/fixtures/cassettes/paypal/one_touch_checkout.yml b/spec/fixtures/cassettes/paypal/one_touch_checkout.yml new file mode 100644 index 00000000..3f1e9375 --- /dev/null +++ b/spec/fixtures/cassettes/paypal/one_touch_checkout.yml @@ -0,0 +1,277 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/client_token + body: + encoding: UTF-8 + string: | + + + 2 + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:09 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"0abbbd99801e6c6eac33f403727c38c8" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 25ef8aae-46ed-42ad-8ea6-702739b6b2da + X-Runtime: + - '0.137755' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAPmUE1gAA6RWX3ObOBB/76fI5L13IEKunknbiW3AaIxcEyxAbyA5Z0AC + X7Ex8OlvhXttcpPrdOYePBmb1e7q9488fO6VvOn2X9uiqT/emr8Ztzf7mjei + qP/8eLuL3Pcfbj9/evfAZbGvT+9PTbWvP727uXnoMnnef9oPGLEEj1k8O/tl + M6wX+CCSsMktfNwr19C/h0qeGaIDX+FjXm+LTeHbZOn3pDxUBIXlJno00jg1 + mcIlWR7KYJQqWD5am6VjpaNjBGOFgtHvWUwqtnyE73NJIrdkane3iXZjMIqC + jPPy2SMDi12DxeFzmmxnQfnYk8Log4UxbNxtv1k2fVA2l83T3UCWTh+MgUEi + 5yvMugRub8LfgShTckWaNLaNBMnqSyQG5nE7UI2VKsNiqjWzJeEigjMlu4gY + t1lMnjNE7S+xaWXq9HW/EiVfBkOKTk2u0gIwKXNkqywWlKsL3B83YhVe+Nh0 + a+Resid7hHlVqmZ3a4WHNJZnscKSxeIgPGqlSXVO0ey0KbdGMMxOgHeZeS7U + BB2xcMXGashKUeSRUNlIG6gpc0/KvNY4zI9rK+3XiHS5Ykdm0SFNwmOO7qa9 + oE+be1TzM/rlMU+eLgWLbZQlGOpNeeULf9/PLy5Fivoj1Bhb6E0T3ALvRbYK + Db4K7tfD7MC96syRe2Ye7vYLu+DKBd5D4IbWMFsK5NprBftEjUGWQZfHsBM6 + HKBmXI9v4R10/8xMJrz81lfuyBE1uEmHfOHf++pgiNV83BQfujQhI0ug10+w + nO6RUCNz3zoPvy9sBPNO+fBr3Ez9YvuQryRonmgs//DrN3p/u8c6ds9p3NvC + kyX/xRn/gU3xDJwJ76DxdXaIliLBMoRdUtVLBvOhl8mghnuuDZ/2Z8+mvVV4 + 5Na8TRO5SWNTan53VijzGDSx2l71fNUX1j38yj1wxDpS4YKV5J5KmVKJvZ1z + dDeRtGMHo23dmGx5+EqdDxZ3QJ8meNV1LyLCq8iZ0V2JB+5KI9rhQxi5X/Ye + KTeOP4Qo7IQT/sVGvMmWkmicdY4I5ZZ7+hLfR609uffcE/d6OenraXbhC9BK + TUEPeM4sjanWdPjKh5oXloD+knnLnmzwqtEJbzZOnILOGP0/GrvWveET8B4t + MzQzxcK+ANaXNL58P8c8CXwSY/tifl7TNl9onfVtbglf7w58GbymEjiBe7Ej + V7Mz+OksHLuLFDUEmg3Z4N8z5bYc7cA38EzJAfLopHWod8oRU8DnmT1NWMFz + OOfRKvruS8gAxUDbRM956eflKx0o88BBJ5CfkInQB7Sp6zPYN4vtehsLfWaa + yw0K+scti6E39BKx3u1H9m5jUuaWvoes9I78X/oDjUMvu9zv5JgbpGPeTvNJ + d8Z28gPgeMwVBl1SN1duARkHfY4KsBu13l9kznX38m1vwaxvOaDvN1OsmLLn + ojW2A89cfU4MyDWTTzUmeLy9ekTvv8L2Fs0qwPbelzSaNAEZzIGH1JLLHIWT + vygljn72ai/IZcgHeG/QYeLpBx8v8xXwhLwCfF9pEd4pwOEJMqUFfR1hf+CW + mHw173gdSubYUqxEx9Vpyn1fawc5eu8TaNbQ75g0wdU110y5X2DxnBgfH36/ + vvffPfz++j+CvwEAAP//AwBjzI7nSAgAAA== + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:14 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/customers + body: + encoding: UTF-8 + string: | + + + dbec3c8b-01eb-4c84-9974-b66ae97766ca + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:24 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"fa41c5145af34ac6905aad524421ee65" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - df5e5087-ac5e-4b1e-9ad3-57db375e392f + X-Runtime: + - '0.351738' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAAiVE1gAA6RTTXPaMBC98ys8vgthEzAwxrQ99BekM51emLW1Nmr04ZHk + BP/7yjaGBJNDJ8d97+2u9HY3PZylCF7RWK7VPozmizBAVWjGVbUPfz3/JJvw + kM3SorFOSzTZLAhSzrJovUo2mzhOqQ86zHPFCZQjPk4Mq7bx3ySXSfmyOi1T + +p7t1CU31hEFEgPFxT50psGQ9pSAz5hCyxpUO8FRAhcTtD5pNa1RwnmCvWFu + uXvQzyA4ZARc4Noa9yHzoeMSwyxeRGsSLUi8eY42uyjexU9/UnpL6PObmv1f + /i1h6N97TkqOgtnrkxh3pADD7KUoGAPt+GdoaxAEikI3yn1UdIKJZAA9nHMh + /MgJVAZR4jCpH2S5/r1IVqskSrbbp+Ui/p7Sh8qxzBctG2pcVo18WLP36Khk + WEIjxla51gJBhVk3wpReyKu435LMOpS5Fu6YNy2ab2Po0Lq536+UDrIxiUuo + kDRGZCfnarujFKxFZ+e5Aa6cd6Dyz3+Dtkum3tvOkKNEd9LsKHSl6eD3vFbV + AdUrN1p1kr0FxXJ99udz7TD2tE1uC8Nr5y/ywZB7jdMvqLJErkzNUjpEI/fF + tRv+bUl3rwoF4Yo73vH3NpcgrPf5kfRax/++G5kq9f119bTgknedG4tEG9ZP + dypM6XRp7zHbg8CYQT+giW23/clm/wAAAP//AwA/PVDq8wQAAA== + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:30 GMT +- request: + method: post + uri: https://api.sandbox.braintreegateway.com/merchants/7rdg92j7bm7fk5h3/transactions + body: + encoding: UTF-8 + string: | + + + 29.99 + + true + + R310468715-YPKS3ANJ + 7m5rpd + 16578822 + sale + + headers: + Accept-Encoding: + - gzip + Accept: + - application/xml + User-Agent: + - Braintree Ruby Gem 2.68.1 + X-Apiversion: + - '4' + Authorization: + - Basic bXdqa2t4d2NwMzJja2huZjphOTI5OGY0M2IzMGM2OTlkYjMwNzJjYzRhMDBmN2Y0OQ== + Content-Type: + - application/xml + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 28 Oct 2016 18:12:30 GMT + Content-Type: + - application/xml; charset=utf-8 + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Authentication: + - basic_auth + X-User: + - 3v249hqtptsg744y + Vary: + - Accept-Encoding + Content-Encoding: + - gzip + Etag: + - W/"1f06d16a8abb48fd1b5629107c91de8a" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 4e9593b7-c55b-42be-8cd0-ca37839d6ef8 + X-Runtime: + - '5.382297' + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: ASCII-8BIT + string: !binary |- + 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 + AA== + http_version: + recorded_at: Fri, 28 Oct 2016 18:12:35 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 463bee38..46cb62e8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,6 +35,14 @@ # Requires factories defined in lib/solidus_paypal_braintree/factories.rb require 'solidus_paypal_braintree/factories' +# Requires poltergeist for feature specs +require 'capybara/poltergeist' +Capybara.register_driver :poltergeist do |app| + # Paypal requires TLS v1.2 for ssl connections + Capybara::Poltergeist::Driver.new(app, { phantomjs_options: ['--ssl-protocol=tlsv1.2'] }) +end +Capybara.javascript_driver = :poltergeist + VCR.configure do |c| c.cassette_library_dir = "spec/fixtures/cassettes" c.hook_into :webmock From 624f7c796004fef15e86dfa1d04d0cdf63c4a38d Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Thu, 3 Nov 2016 16:23:35 -0700 Subject: [PATCH 05/16] Use configuration object for Paypal initialization --- .../spree/frontend/solidus_paypal_braintree.js | 16 ++++++++++++---- .../solidus_paypal_braintree_frontend.js | 12 ++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js index 16c07a47..429e6af2 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js @@ -63,10 +63,18 @@ window.SolidusPaypalBraintree = { }); }, - initializePaypalSession: function(paypalInstance, paypalButton, paypalOptions, submitCallback) { - paypalButton.removeAttribute('disabled'); - paypalButton.addEventListener('click', function(event) { - paypalInstance.tokenize(paypalOptions, function(tokenizeErr, payload) { + /* Initializes and begins the Paypal session + * + * @param config Configuration settings for the session + * @param config.paypalInstance {object} The Paypal instance returned by Braintree + * @param config.paypalButton {object} The button DOM element + * @param config.paypalOptions {object} Configuration options for Paypal + * @param submitCallback Callback function after successful tokenization + */ + initializePaypalSession: function(config, submitCallback) { + config.paypalButton.removeAttribute('disabled'); + config.paypalButton.addEventListener('click', function(event) { + config.paypalInstance.tokenize(config.paypalOptions, function(tokenizeErr, payload) { if (tokenizeErr) { if (tokenizeErr.type !== 'CUSTOMER') { console.error('Error tokenizing:', tokenizeErr); diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js index 28268581..9f849c1c 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -9,7 +9,11 @@ initializePaypal = function(authToken, paymentMethodId) { } else { var paypalOptions = {flow: 'vault', enableShippingAddress: true} } - window.SolidusPaypalBraintree.initializePaypalSession(paypalInstance, paypalButton, paypalOptions, submitBraintreeTransaction); + window.SolidusPaypalBraintree.initializePaypalSession({ + paypalInstance: paypalInstance, + paypalButton: paypalButton, + paypalOptions: paypalOptions, + }, submitBraintreeTransaction); }); }); }; @@ -21,7 +25,11 @@ initializePaypalCredit = function(authToken, paymentMethodId) { var paypalButton = document.querySelector('#paypal-credit-button'); var address = JSON.parse(document.querySelector('#shipping_address').value); var paypalOptions = {flow: 'checkout', amount: amount, currency: currency, shippingAddressOverride: address, shippingAddressEditable: false, enableShippingAddress: true} - window.SolidusPaypalBraintree.initializePaypalSession(paypalInstance, paypalButton, paypalOptions, submitBraintreeTransaction); + window.SolidusPaypalBraintree.initializePaypalSession({ + paypalInstance: paypalInstance, + paypalButton: paypalButton, + paypalOptions: paypalOptions, + }, submitBraintreeTransaction); }); }); }; From 17c1d855abc1ab198489169701de4156d7fb2a03 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Fri, 4 Nov 2016 08:33:44 -0700 Subject: [PATCH 06/16] Don't reassign payment method This view only gets rendered if the payment method is active, and the payment method gets passed to it as a local variable already. --- .../spree/checkout/payment/_paypal_braintree.html.erb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb index e7d43536..5a1d151f 100644 --- a/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +++ b/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb @@ -1,7 +1,4 @@ -<% payment_method = current_store.payment_methods.where(active: true).find_by_type('SolidusPaypalBraintree::Gateway') %> -<% if payment_method %> - <% if current_order.valid? %> - <%= render partial: 'solidus_paypal_braintree/address_field', locals: { shipping_address: current_order.shipping_address } %> - <% end %> - <%= render partial: 'solidus_paypal_braintree/paypal_button' %> +<% if current_order.valid? %> + <%= render partial: 'solidus_paypal_braintree/address_field', locals: { shipping_address: current_order.shipping_address } %> <% end %> +<%= render partial: 'solidus_paypal_braintree/paypal_button' %> From 0cb3091cfaad61742f8aacfeedd5ee26f064f4f7 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Fri, 4 Nov 2016 14:53:55 -0700 Subject: [PATCH 07/16] Use latest Braintree JS --- .../spree/frontend/solidus_paypal_braintree_frontend.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js index 9f849c1c..e774ca25 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -73,9 +73,9 @@ submitBraintreeTransaction = function(payload) { $(document).ready(function() { if (document.getElementById("empty-cart")) { $.when( - $.getScript("https://js.braintreegateway.com/web/3.4.0/js/client.min.js"), - $.getScript("https://js.braintreegateway.com/web/3.4.0/js/paypal.min.js"), - $.getScript("https://js.braintreegateway.com/web/3.4.0/js/data-collector.min.js"), + $.getScript("https://js.braintreegateway.com/web/3.5.0/js/client.min.js"), + $.getScript("https://js.braintreegateway.com/web/3.5.0/js/paypal.min.js"), + $.getScript("https://js.braintreegateway.com/web/3.5.0/js/data-collector.min.js"), $.Deferred(function( deferred ){ $( deferred.resolve ); }) From a9badf59b8cae826d43fa5c4bad55e39b8cddbf9 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Fri, 4 Nov 2016 14:54:11 -0700 Subject: [PATCH 08/16] Clean up formatting --- .../solidus_paypal_braintree_frontend.js | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js index e774ca25..9bda4aae 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -43,19 +43,26 @@ submitBraintreeTransaction = function(payload) { var first_name = payload.details.firstName; var last_name = payload.details.lastName; } - var transactionParams = { "payment_method_id" : window.paymentMethodId, - "transaction" : { "email" : payload.details.email, - "phone" : payload.details.phone, - "nonce" : payload.nonce, - "payment_type" : payload.type, - "address_attributes" : { "first_name" : first_name, - "last_name" : last_name, - "address_line_1" : payload.details.shippingAddress.line1, - "address_line_2" : payload.details.shippingAddress.line2, - "city" : payload.details.shippingAddress.city, - "state_code" : payload.details.shippingAddress.state, - "zip" : payload.details.shippingAddress.postalCode, - "country_code" : payload.details.shippingAddress.countryCode } } } + var transactionParams = { + "payment_method_id" : window.paymentMethodId, + "transaction" : { + "email" : payload.details.email, + "phone" : payload.details.phone, + "nonce" : payload.nonce, + "payment_type" : payload.type, + "address_attributes" : { + "first_name" : first_name, + "last_name" : last_name, + "address_line_1" : payload.details.shippingAddress.line1, + "address_line_2" : payload.details.shippingAddress.line2, + "city" : payload.details.shippingAddress.city, + "state_code" : payload.details.shippingAddress.state, + "zip" : payload.details.shippingAddress.postalCode, + "country_code" : payload.details.shippingAddress.countryCode + } + } + } + Spree.ajax({ url: Spree.pathFor("solidus_paypal_braintree/transactions"), type: 'POST', From 04c81ee9a390622d2da17b9fc47cfc4a1c4171ec Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Mon, 7 Nov 2016 10:33:43 -0800 Subject: [PATCH 09/16] Add translations for payment types to source --- app/models/solidus_paypal_braintree/source.rb | 4 ++++ config/locales/en.yml | 3 +++ .../solidus_paypal_braintree/source_spec.rb | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/app/models/solidus_paypal_braintree/source.rb b/app/models/solidus_paypal_braintree/source.rb index be51bc54..cde40ba3 100644 --- a/app/models/solidus_paypal_braintree/source.rb +++ b/app/models/solidus_paypal_braintree/source.rb @@ -25,4 +25,8 @@ def can_void?(payment) def can_credit?(payment) payment.completed? && payment.credit_allowed > 0 end + + def friendly_payment_type + I18n.t(payment_type.underscore, scope: "solidus_paypal_braintree.payment_type") + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 547bfb1b..cdec84f8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,3 +2,6 @@ en: solidus_paypal_braintree: nonce: Nonce token: Token + payment_type: + apple_pay_card: Apple Pay + pay_pal_account: PayPal diff --git a/spec/models/solidus_paypal_braintree/source_spec.rb b/spec/models/solidus_paypal_braintree/source_spec.rb index d010e83d..68ddcb42 100644 --- a/spec/models/solidus_paypal_braintree/source_spec.rb +++ b/spec/models/solidus_paypal_braintree/source_spec.rb @@ -86,4 +86,24 @@ it { is_expected.not_to be } end end + + describe "#friendly_payment_type" do + subject { described_class.new(payment_type: type).friendly_payment_type } + + context "when then payment type is PayPal" do + let(:type) { "PayPalAccount" } + + it "returns the translated payment type" do + expect(subject).to eq "PayPal" + end + end + + context "when the payment type is Apple Pay" do + let(:type) { "ApplePayCard" } + + it "returns the translated payment type" do + expect(subject).to eq "Apple Pay" + end + end + end end From a35e6b39775ffc71d1d046523d113a224773b007 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Mon, 7 Nov 2016 10:37:06 -0800 Subject: [PATCH 10/16] Add predicate methods to check for payment type --- app/models/solidus_paypal_braintree/source.rb | 11 +++++++ .../solidus_paypal_braintree/source_spec.rb | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/app/models/solidus_paypal_braintree/source.rb b/app/models/solidus_paypal_braintree/source.rb index cde40ba3..f4629482 100644 --- a/app/models/solidus_paypal_braintree/source.rb +++ b/app/models/solidus_paypal_braintree/source.rb @@ -1,4 +1,7 @@ class SolidusPaypalBraintree::Source < ApplicationRecord + PAYPAL = "PayPalAccount" + APPLE_PAY = "ApplePayCard" + belongs_to :user, class_name: "Spree::User" belongs_to :payment_method, class_name: 'Spree::PaymentMethod' has_many :payments, as: :source, class_name: "Spree::Payment" @@ -29,4 +32,12 @@ def can_credit?(payment) def friendly_payment_type I18n.t(payment_type.underscore, scope: "solidus_paypal_braintree.payment_type") end + + def apple_pay? + payment_type == APPLE_PAY + end + + def paypal? + payment_type == PAYPAL + end end diff --git a/spec/models/solidus_paypal_braintree/source_spec.rb b/spec/models/solidus_paypal_braintree/source_spec.rb index 68ddcb42..c5647080 100644 --- a/spec/models/solidus_paypal_braintree/source_spec.rb +++ b/spec/models/solidus_paypal_braintree/source_spec.rb @@ -106,4 +106,36 @@ end end end + + describe "#apple_pay?" do + subject { described_class.new(payment_type: type).apple_pay? } + + context "when the payment type is Apple Pay" do + let(:type) { "ApplePayCard" } + + it { is_expected.to be true } + end + + context "when the payment type is not PayPal" do + let(:type) { "DogeCoin" } + + it { is_expected.to be false } + end + end + + describe "#paypal?" do + subject { described_class.new(payment_type: type).paypal? } + + context "when the payment type is PayPal" do + let(:type) { "PayPalAccount" } + + it { is_expected.to be true } + end + + context "when the payment type is not PayPal" do + let(:type) { "MonopolyMoney" } + + it { is_expected.to be false } + end + end end From 8bcef15411cdd8781acef608a786268c6aa41d4c Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Mon, 7 Nov 2016 13:48:49 -0800 Subject: [PATCH 11/16] Allow user to provide callback function With the current implementation we essentially "swallow" JS errors and don't allow the user to do anything with them. Specifying callback function allows users to determine what they do in both the error and success cases. --- .../frontend/solidus_paypal_braintree.js | 23 ++--- .../solidus_paypal_braintree_frontend.js | 87 ++++++++++--------- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js index 429e6af2..c56233c5 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js @@ -1,6 +1,14 @@ // Placeholder manifest file. // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' +/** + * Callback definition for tokenization + * + * @callback tokenizeCallback + * @param {object} The error returned by Braintree if one occurred + * @param {object} The payload returned by Braintree for successful tokenization + */ + window.SolidusPaypalBraintree = { APPLE_PAY_API_VERSION: 1, @@ -69,20 +77,13 @@ window.SolidusPaypalBraintree = { * @param config.paypalInstance {object} The Paypal instance returned by Braintree * @param config.paypalButton {object} The button DOM element * @param config.paypalOptions {object} Configuration options for Paypal - * @param submitCallback Callback function after successful tokenization + * @param config.error {tokenizeErrorCallback} Callback function for tokenize errors + * @param {tokenizeCallback} callback Callback function for tokenization */ - initializePaypalSession: function(config, submitCallback) { + initializePaypalSession: function(config, callback) { config.paypalButton.removeAttribute('disabled'); config.paypalButton.addEventListener('click', function(event) { - config.paypalInstance.tokenize(config.paypalOptions, function(tokenizeErr, payload) { - if (tokenizeErr) { - if (tokenizeErr.type !== 'CUSTOMER') { - console.error('Error tokenizing:', tokenizeErr); - } - return; - } - submitCallback(payload); - }); + config.paypalInstance.tokenize(config.paypalOptions, callback); }, false); }, diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js index 9bda4aae..5ca5d17a 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -13,7 +13,7 @@ initializePaypal = function(authToken, paymentMethodId) { paypalInstance: paypalInstance, paypalButton: paypalButton, paypalOptions: paypalOptions, - }, submitBraintreeTransaction); + }, onTokenizeCallback); }); }); }; @@ -29,52 +29,59 @@ initializePaypalCredit = function(authToken, paymentMethodId) { paypalInstance: paypalInstance, paypalButton: paypalButton, paypalOptions: paypalOptions, - }, submitBraintreeTransaction); + }, onTokenizeCallback); }); }); }; -submitBraintreeTransaction = function(payload) { - if (payload.details.shippingAddress.recipientName) { - var first_name = payload.details.shippingAddress.recipientName.split(" ")[0]; - var last_name = payload.details.shippingAddress.recipientName.split(" ")[1]; - } - if (first_name == null || last_name == null) { - var first_name = payload.details.firstName; - var last_name = payload.details.lastName; - } - var transactionParams = { - "payment_method_id" : window.paymentMethodId, - "transaction" : { - "email" : payload.details.email, - "phone" : payload.details.phone, - "nonce" : payload.nonce, - "payment_type" : payload.type, - "address_attributes" : { - "first_name" : first_name, - "last_name" : last_name, - "address_line_1" : payload.details.shippingAddress.line1, - "address_line_2" : payload.details.shippingAddress.line2, - "city" : payload.details.shippingAddress.city, - "state_code" : payload.details.shippingAddress.state, - "zip" : payload.details.shippingAddress.postalCode, - "country_code" : payload.details.shippingAddress.countryCode +onTokenizeCallback = function(tokenizeErr, payload) { + if (tokenizeErr) { + if (tokenizeErr.type !== 'CUSTOMER') { + console.error('Error tokenizing:', tokenizeErr); + } + return; + } else { + if (payload.details.shippingAddress.recipientName) { + var first_name = payload.details.shippingAddress.recipientName.split(" ")[0]; + var last_name = payload.details.shippingAddress.recipientName.split(" ")[1]; + } + if (first_name == null || last_name == null) { + var first_name = payload.details.firstName; + var last_name = payload.details.lastName; + } + var transactionParams = { + "payment_method_id" : window.paymentMethodId, + "transaction" : { + "email" : payload.details.email, + "phone" : payload.details.phone, + "nonce" : payload.nonce, + "payment_type" : payload.type, + "address_attributes" : { + "first_name" : first_name, + "last_name" : last_name, + "address_line_1" : payload.details.shippingAddress.line1, + "address_line_2" : payload.details.shippingAddress.line2, + "city" : payload.details.shippingAddress.city, + "state_code" : payload.details.shippingAddress.state, + "zip" : payload.details.shippingAddress.postalCode, + "country_code" : payload.details.shippingAddress.countryCode + } } } - } - Spree.ajax({ - url: Spree.pathFor("solidus_paypal_braintree/transactions"), - type: 'POST', - dataType: 'json', - data: transactionParams, - success: function(response) { - window.location.href = Spree.pathFor("checkout/confirm"); - }, - error: function(xhr) { - console.error("Error submitting transaction") - }, - }); + Spree.ajax({ + url: Spree.pathFor("solidus_paypal_braintree/transactions"), + type: 'POST', + dataType: 'json', + data: transactionParams, + success: function(response) { + window.location.href = Spree.pathFor("checkout/confirm"); + }, + error: function(xhr) { + console.error("Error submitting transaction") + }, + }); + } }; $(document).ready(function() { From 0c29e3c33ed1d59a7186d559cc9bd2d4b9f5e999 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Mon, 7 Nov 2016 17:05:45 -0800 Subject: [PATCH 12/16] Use helper function to build transaction params --- .../solidus_paypal_braintree_frontend.js | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js index 5ca5d17a..4197ec3f 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -39,43 +39,16 @@ onTokenizeCallback = function(tokenizeErr, payload) { if (tokenizeErr.type !== 'CUSTOMER') { console.error('Error tokenizing:', tokenizeErr); } - return; } else { - if (payload.details.shippingAddress.recipientName) { - var first_name = payload.details.shippingAddress.recipientName.split(" ")[0]; - var last_name = payload.details.shippingAddress.recipientName.split(" ")[1]; - } - if (first_name == null || last_name == null) { - var first_name = payload.details.firstName; - var last_name = payload.details.lastName; - } - var transactionParams = { - "payment_method_id" : window.paymentMethodId, - "transaction" : { - "email" : payload.details.email, - "phone" : payload.details.phone, - "nonce" : payload.nonce, - "payment_type" : payload.type, - "address_attributes" : { - "first_name" : first_name, - "last_name" : last_name, - "address_line_1" : payload.details.shippingAddress.line1, - "address_line_2" : payload.details.shippingAddress.line2, - "city" : payload.details.shippingAddress.city, - "state_code" : payload.details.shippingAddress.state, - "zip" : payload.details.shippingAddress.postalCode, - "country_code" : payload.details.shippingAddress.countryCode - } - } - } + params = transactionParams(payload); Spree.ajax({ url: Spree.pathFor("solidus_paypal_braintree/transactions"), type: 'POST', dataType: 'json', - data: transactionParams, + data: params, success: function(response) { - window.location.href = Spree.pathFor("checkout/confirm"); + window.location.href = response.redirectUrl; }, error: function(xhr) { console.error("Error submitting transaction") @@ -84,6 +57,41 @@ onTokenizeCallback = function(tokenizeErr, payload) { } }; +addressParams = function(payload) { + if (payload.details.shippingAddress.recipientName) { + var first_name = payload.details.shippingAddress.recipientName.split(" ")[0]; + var last_name = payload.details.shippingAddress.recipientName.split(" ")[1]; + } + if (first_name == null || last_name == null) { + var first_name = payload.details.firstName; + var last_name = payload.details.lastName; + } + + return { + "first_name" : first_name, + "last_name" : last_name, + "address_line_1" : payload.details.shippingAddress.line1, + "address_line_2" : payload.details.shippingAddress.line2, + "city" : payload.details.shippingAddress.city, + "state_code" : payload.details.shippingAddress.state, + "zip" : payload.details.shippingAddress.postalCode, + "country_code" : payload.details.shippingAddress.countryCode + } +}; + +transactionParams = function(payload) { + return { + "payment_method_id" : window.paymentMethodId, + "transaction" : { + "email" : payload.details.email, + "phone" : payload.details.phone, + "nonce" : payload.nonce, + "payment_type" : payload.type, + "address_attributes" : addressParams(payload) + } + } +}; + $(document).ready(function() { if (document.getElementById("empty-cart")) { $.when( From d0cffc168553a40bd15dc49ef54fa44dced0a7f1 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Tue, 8 Nov 2016 18:46:36 -0800 Subject: [PATCH 13/16] Refactor frontend PayPal JS This tries to simplify what was happening before. I created an instantiable PayPal object that encapsulates most of the logic for creating the button. Instead of hardcoding the options passed to the paypal tokenize call in initializePaypal and initializePaypalCredit, I just use one function that allows the user to pass in the configuration options. --- .../frontend/solidus_paypal_braintree.js | 36 ---- .../solidus_paypal_braintree_frontend.js | 185 ++++++++++++------ .../_paypal_button.html.erb | 21 +- .../_paypal_credit_button.html.erb | 23 ++- 4 files changed, 165 insertions(+), 100 deletions(-) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js index c56233c5..28de0ecf 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js @@ -1,14 +1,6 @@ // Placeholder manifest file. // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' -/** - * Callback definition for tokenization - * - * @callback tokenizeCallback - * @param {object} The error returned by Braintree if one occurred - * @param {object} The payload returned by Braintree for successful tokenization - */ - window.SolidusPaypalBraintree = { APPLE_PAY_API_VERSION: 1, @@ -59,34 +51,6 @@ window.SolidusPaypalBraintree = { }); }, - setupPaypal: function(braintreeClient, readyCallback) { - braintree.paypal.create({ - client: braintreeClient - }, function (paypalErr, paypalInstance) { - if (paypalErr) { - console.error("Error creating PayPal:", paypalErr); - return; - } - readyCallback(paypalInstance); - }); - }, - - /* Initializes and begins the Paypal session - * - * @param config Configuration settings for the session - * @param config.paypalInstance {object} The Paypal instance returned by Braintree - * @param config.paypalButton {object} The button DOM element - * @param config.paypalOptions {object} Configuration options for Paypal - * @param config.error {tokenizeErrorCallback} Callback function for tokenize errors - * @param {tokenizeCallback} callback Callback function for tokenization - */ - initializePaypalSession: function(config, callback) { - config.paypalButton.removeAttribute('disabled'); - config.paypalButton.addEventListener('click', function(event) { - config.paypalInstance.tokenize(config.paypalOptions, callback); - }, false); - }, - setupApplePay: function(braintreeClient, merchantId, readyCallback) { if(window.ApplePaySession) { var promise = ApplePaySession.canMakePaymentsWithActiveCard(merchantId); diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js index 4197ec3f..df8a1586 100644 --- a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js +++ b/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js @@ -1,46 +1,81 @@ -initializePaypal = function(authToken, paymentMethodId) { - window.paymentMethodId = paymentMethodId; - window.SolidusPaypalBraintree.initializeWithDataCollector(authToken, function(clientInstance) { - window.SolidusPaypalBraintree.setupPaypal(clientInstance, function(paypalInstance) { - var paypalButton = document.querySelector('#paypal-button'); - if (document.querySelector('#shipping_address')) { - var address = JSON.parse(document.querySelector('#shipping_address').value); - var paypalOptions = {flow: 'vault', shippingAddressOverride: address, shippingAddressEditable: false, enableShippingAddress: true} - } else { - var paypalOptions = {flow: 'vault', enableShippingAddress: true} - } - window.SolidusPaypalBraintree.initializePaypalSession({ - paypalInstance: paypalInstance, - paypalButton: paypalButton, - paypalOptions: paypalOptions, - }, onTokenizeCallback); +/** + * Constructor for PayPal button object + * @constructor + * @param {object} element - The DOM element of your PayPal button + */ +function PaypalButton(element) { + this.element = element; +} + +/** + * Creates the PayPal session using the provided options and enables the button + * + * @param {object} options - The options passed to tokenize when constructing + * the PayPal instance + * + * See {@link https://braintree.github.io/braintree-web/3.5.0/PayPal.html#tokenize} + */ +PaypalButton.prototype.initialize = function(options) { + var self = this; + + /* This sets the payment method id returned by fetchToken on the PaypalButton + * instance so that we can use it to build the transaction params later. */ + SolidusPaypalBraintree.fetchToken(function(token, paymentMethodId) { + self.paymentMethodId = paymentMethodId; + + SolidusPaypalBraintree.initializeWithDataCollector(token, function(client) { + self.createPaypalInstance(client, function(paypal) { + + self.initializePaypalSession({ + paypalInstance: paypal, + paypalButton: self.element, + paypalOptions: options + }, self.tokenizeCallback.bind(self)); + }); + }); }); }; -initializePaypalCredit = function(authToken, paymentMethodId) { - window.paymentMethodId = paymentMethodId; - window.SolidusPaypalBraintree.initializeWithDataCollector(authToken, function(clientInstance) { - window.SolidusPaypalBraintree.setupPaypal(clientInstance, function(paypalInstance) { - var paypalButton = document.querySelector('#paypal-credit-button'); - var address = JSON.parse(document.querySelector('#shipping_address').value); - var paypalOptions = {flow: 'checkout', amount: amount, currency: currency, shippingAddressOverride: address, shippingAddressEditable: false, enableShippingAddress: true} - window.SolidusPaypalBraintree.initializePaypalSession({ - paypalInstance: paypalInstance, - paypalButton: paypalButton, - paypalOptions: paypalOptions, - }, onTokenizeCallback); - }); +PaypalButton.prototype.createPaypalInstance = function(braintreeClient, readyCallback) { + braintree.paypal.create({ + client: braintreeClient + }, function (paypalErr, paypalInstance) { + if (paypalErr) { + console.error("Error creating PayPal:", paypalErr); + return; + } + readyCallback(paypalInstance); }); }; -onTokenizeCallback = function(tokenizeErr, payload) { +/* Initializes and begins the Paypal session + * + * @param config Configuration settings for the session + * @param config.paypalInstance {object} The Paypal instance returned by Braintree + * @param config.paypalButton {object} The button DOM element + * @param config.paypalOptions {object} Configuration options for Paypal + * @param config.error {tokenizeErrorCallback} Callback function for tokenize errors + * @param {tokenizeCallback} callback Callback function for tokenization + */ +PaypalButton.prototype.initializePaypalSession = function(config, callback) { + config.paypalButton.removeAttribute('disabled'); + config.paypalButton.addEventListener('click', function(event) { + config.paypalInstance.tokenize(config.paypalOptions, callback); + }, false); + }, + +/** + * Default callback function for when tokenization completes + * + * @param {object|null} tokenizeErr - The error returned by Braintree on failure + * @param {object} payload - The payload returned by Braintree on success + */ +PaypalButton.prototype.tokenizeCallback = function(tokenizeErr, payload) { if (tokenizeErr) { - if (tokenizeErr.type !== 'CUSTOMER') { - console.error('Error tokenizing:', tokenizeErr); - } + console.error('Error tokenizing:', tokenizeErr); } else { - params = transactionParams(payload); + var params = this.transactionParams(payload); Spree.ajax({ url: Spree.pathFor("solidus_paypal_braintree/transactions"), @@ -57,7 +92,41 @@ onTokenizeCallback = function(tokenizeErr, payload) { } }; -addressParams = function(payload) { +/** + * Assigns a new callback function for when tokenization completes + * + * @callback callback - The callback function to assign + */ +PaypalButton.prototype.setTokenizeCallback = function(callback) { + this.tokenizeCallback = callback; +}; + +/** + * Builds the transaction parameters to submit to Solidus for the given + * payload returned by Braintree + * + * @param {object} payload - The payload returned by Braintree after tokenization + */ +PaypalButton.prototype.transactionParams = function(payload) { + return { + "payment_method_id" : this.paymentMethodId, + "transaction" : { + "email" : payload.details.email, + "phone" : payload.details.phone, + "nonce" : payload.nonce, + "payment_type" : payload.type, + "address_attributes" : this.addressParams(payload) + } + } +}; + +/** + * Builds the address parameters to submit to Solidus using the payload + * returned by Braintree + * + * @param {object} payload - The payload returned by Braintree after tokenization + */ +PaypalButton.prototype.addressParams = function(payload) { if (payload.details.shippingAddress.recipientName) { var first_name = payload.details.shippingAddress.recipientName.split(" ")[0]; var last_name = payload.details.shippingAddress.recipientName.split(" ")[1]; @@ -79,19 +148,6 @@ addressParams = function(payload) { } }; -transactionParams = function(payload) { - return { - "payment_method_id" : window.paymentMethodId, - "transaction" : { - "email" : payload.details.email, - "phone" : payload.details.phone, - "nonce" : payload.nonce, - "payment_type" : payload.type, - "address_attributes" : addressParams(payload) - } - } -}; - $(document).ready(function() { if (document.getElementById("empty-cart")) { $.when( @@ -102,20 +158,25 @@ $(document).ready(function() { $( deferred.resolve ); }) ).done(function() { - $(window).load(function() { - $(' @@ -15,5 +17,22 @@ data-button_disabled="true" > diff --git a/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb b/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb index 05971b2a..ae6b3d96 100644 --- a/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb +++ b/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb @@ -1,3 +1,5 @@ +<% address = current_order.ship_address %> + @@ -15,5 +17,24 @@ data-button_disabled="true" > From d179a8c75880c31c3ba787f241e8cd41992b8cea Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Wed, 9 Nov 2016 15:14:09 -0800 Subject: [PATCH 14/16] Make capybara drivers configurable via metadata --- Gemfile | 2 ++ spec/spec_helper.rb | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index fed00725..6c0a4b76 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,8 @@ gem 'solidus_auth_devise', '~> 1.0' group :development, :test do gem "pry-rails" + gem "selenium-webdriver" + gem "chromedriver-helper" end gemspec diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 46cb62e8..190294ad 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -41,7 +41,9 @@ # Paypal requires TLS v1.2 for ssl connections Capybara::Poltergeist::Driver.new(app, { phantomjs_options: ['--ssl-protocol=tlsv1.2'] }) end -Capybara.javascript_driver = :poltergeist +Capybara.register_driver :chrome do |app| + Capybara::Selenium::Driver.new(app, browser: :chrome) +end VCR.configure do |c| c.cassette_library_dir = "spec/fixtures/cassettes" @@ -91,6 +93,10 @@ def create_gateway(opts = {}) config.include Spree::TestingSupport::UrlHelpers config.include BraintreeHelpers + config.before(:each, type: :feature, js: true) do |ex| + Capybara.current_driver = ex.metadata[:driver] || :poltergeist + end + config.before :suite do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with :truncation From d7d3550a2fdf591fb99f2d9dfcd2ea995dc8ec65 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Wed, 9 Nov 2016 15:50:19 -0800 Subject: [PATCH 15/16] Rename frontend JS to paypal_button.js --- .../{solidus_paypal_braintree_frontend.js => paypal_button.js} | 0 .../solidus_paypal_braintree/install/install_generator.rb | 2 +- lib/solidus_paypal_braintree/engine.rb | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/assets/javascripts/spree/frontend/{solidus_paypal_braintree_frontend.js => paypal_button.js} (100%) diff --git a/app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js b/app/assets/javascripts/spree/frontend/paypal_button.js similarity index 100% rename from app/assets/javascripts/spree/frontend/solidus_paypal_braintree_frontend.js rename to app/assets/javascripts/spree/frontend/paypal_button.js diff --git a/lib/generators/solidus_paypal_braintree/install/install_generator.rb b/lib/generators/solidus_paypal_braintree/install/install_generator.rb index 045bf646..85597ac0 100644 --- a/lib/generators/solidus_paypal_braintree/install/install_generator.rb +++ b/lib/generators/solidus_paypal_braintree/install/install_generator.rb @@ -5,7 +5,7 @@ class InstallGenerator < Rails::Generators::Base def add_javascripts append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_paypal_braintree\n" - append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_paypal_braintree_frontend\n" + append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/paypal_button\n" append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_paypal_braintree\n" end diff --git a/lib/solidus_paypal_braintree/engine.rb b/lib/solidus_paypal_braintree/engine.rb index f191d5ef..bcd356ee 100644 --- a/lib/solidus_paypal_braintree/engine.rb +++ b/lib/solidus_paypal_braintree/engine.rb @@ -28,7 +28,7 @@ def self.frontend_available? if frontend_available? config.assets.precompile += [ 'spree/frontend/solidus_paypal_braintree', - 'spree/frontend/solidus_paypal_braintree_frontend' + 'spree/frontend/paypal_button' ] paths["app/controllers"] << "lib/controllers/frontend" paths["app/views"] << "lib/views/frontend" From 00bf8c4b9eed29ab537dffc7baff09657b31a185 Mon Sep 17 00:00:00 2001 From: Luuk Veenis Date: Thu, 10 Nov 2016 08:51:42 -0800 Subject: [PATCH 16/16] Remove separate paypal button and address views --- .../_address_field.html.erb | 9 ---- .../_paypal_button.html.erb | 38 -------------- .../_paypal_credit_button.html.erb | 40 --------------- .../payment/_paypal_braintree.html.erb | 42 ++++++++++++++-- .../payment/_paypal_credit_braintree.html.erb | 49 +++++++++++++++---- 5 files changed, 78 insertions(+), 100 deletions(-) delete mode 100644 lib/views/frontend/solidus_paypal_braintree/_address_field.html.erb delete mode 100644 lib/views/frontend/solidus_paypal_braintree/_paypal_button.html.erb delete mode 100644 lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb diff --git a/lib/views/frontend/solidus_paypal_braintree/_address_field.html.erb b/lib/views/frontend/solidus_paypal_braintree/_address_field.html.erb deleted file mode 100644 index 3e06eeaa..00000000 --- a/lib/views/frontend/solidus_paypal_braintree/_address_field.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% address = {line1: shipping_address.address1, - line2: shipping_address.address2, - city: shipping_address.city, - state: shipping_address.state.name, - postalCode: shipping_address.zipcode, - countryCode: shipping_address.country.iso, - phone: shipping_address.phone, - recipientName: "#{shipping_address.firstname} #{shipping_address.lastname}"}.to_json %> -<%= hidden_field_tag 'shipping_address', address %> diff --git a/lib/views/frontend/solidus_paypal_braintree/_paypal_button.html.erb b/lib/views/frontend/solidus_paypal_braintree/_paypal_button.html.erb deleted file mode 100644 index eb97dc0c..00000000 --- a/lib/views/frontend/solidus_paypal_braintree/_paypal_button.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -<% address = current_order.ship_address %> - - - - - - - - - diff --git a/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb b/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb deleted file mode 100644 index ae6b3d96..00000000 --- a/lib/views/frontend/solidus_paypal_braintree/_paypal_credit_button.html.erb +++ /dev/null @@ -1,40 +0,0 @@ -<% address = current_order.ship_address %> - - - - - - - - - diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb index 5a1d151f..eb97dc0c 100644 --- a/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +++ b/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb @@ -1,4 +1,38 @@ -<% if current_order.valid? %> - <%= render partial: 'solidus_paypal_braintree/address_field', locals: { shipping_address: current_order.shipping_address } %> -<% end %> -<%= render partial: 'solidus_paypal_braintree/paypal_button' %> +<% address = current_order.ship_address %> + + + + + + + + + diff --git a/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb b/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb index bf4da10b..ae6b3d96 100644 --- a/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb +++ b/lib/views/frontend/spree/checkout/payment/_paypal_credit_braintree.html.erb @@ -1,9 +1,40 @@ -<% payment_method = current_store.payment_methods.where(active: true).find_by_type('SolidusPaypalBraintree::Gateway') %> -<% if payment_method %> - <%= render partial: 'solidus_paypal_braintree/address_field', locals: { shipping_address: current_order.shipping_address } %> - - <%= render partial: 'solidus_paypal_braintree/paypal_credit_button' %> -<% end %> +<% address = current_order.ship_address %> + + + + + + + + +