solidus_stripe 1.0.0 → 1.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_release.yml +2 -5
  3. data/.github/stale.yml +17 -0
  4. data/.gitignore +11 -4
  5. data/.rspec +2 -1
  6. data/.rubocop.yml +2 -319
  7. data/Gemfile +16 -9
  8. data/LICENSE +26 -0
  9. data/README.md +58 -1
  10. data/Rakefile +3 -20
  11. data/app/assets/javascripts/solidus_stripe/stripe-init.js +1 -0
  12. data/app/assets/javascripts/solidus_stripe/stripe-init/base.js +180 -0
  13. data/app/controllers/solidus_stripe/intents_controller.rb +52 -0
  14. data/app/controllers/solidus_stripe/payment_request_controller.rb +42 -0
  15. data/app/controllers/spree/stripe_controller.rb +13 -0
  16. data/app/models/solidus_stripe/address_from_params_service.rb +57 -0
  17. data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +46 -0
  18. data/app/models/solidus_stripe/shipping_rates_service.rb +46 -0
  19. data/app/models/spree/payment_method/stripe_credit_card.rb +57 -8
  20. data/bin/console +17 -0
  21. data/bin/rails +12 -4
  22. data/bin/setup +8 -0
  23. data/config/routes.rb +11 -0
  24. data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +5 -0
  25. data/lib/generators/solidus_stripe/install/install_generator.rb +3 -13
  26. data/lib/solidus_stripe/engine.rb +14 -2
  27. data/lib/solidus_stripe/version.rb +1 -1
  28. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +2 -0
  29. data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +13 -12
  30. data/lib/views/frontend/spree/checkout/payment/v3/_elements_js.html.erb +28 -0
  31. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +42 -0
  32. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +5 -0
  33. data/lib/views/frontend/spree/checkout/payment/v3/_intents_js.html.erb +48 -0
  34. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +3 -131
  35. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +92 -0
  36. data/solidus_stripe.gemspec +15 -20
  37. data/spec/features/stripe_checkout_spec.rb +196 -35
  38. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +62 -0
  39. data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +65 -0
  40. data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +54 -0
  41. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +44 -5
  42. data/spec/spec_helper.rb +9 -7
  43. metadata +38 -136
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class PrepareOrderForPaymentService
5
+ attr_reader :order, :address, :user, :email, :shipping_id
6
+
7
+ def initialize(address, controller)
8
+ @address = address
9
+ @order = controller.current_order
10
+ @user = controller.spree_current_user
11
+ @email = controller.params[:email]
12
+ @shipping_id = controller.params[:shipping_option][:id]
13
+ end
14
+
15
+ def call
16
+ set_order_addresses
17
+ manage_guest_checkout
18
+ advance_order_to_payment_state
19
+ end
20
+
21
+ private
22
+
23
+ def set_shipping_rate
24
+ order.shipments.each do |shipment|
25
+ rate = shipment.shipping_rates.find_by(shipping_method: shipping_id)
26
+ shipment.selected_shipping_rate_id = rate.id
27
+ end
28
+ end
29
+
30
+ def set_order_addresses
31
+ order.ship_address = address
32
+ order.bill_address ||= address
33
+ end
34
+
35
+ def manage_guest_checkout
36
+ order.email = email unless user
37
+ end
38
+
39
+ def advance_order_to_payment_state
40
+ while !order.payment?
41
+ set_shipping_rate if order.state == "delivery"
42
+ order.next || break
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class ShippingRatesService
5
+ attr_reader :order, :user, :shipping_address_params
6
+
7
+ def initialize(order, user, shipping_address_params)
8
+ @order = order
9
+ @user = user
10
+ @shipping_address_params = shipping_address_params
11
+ end
12
+
13
+ def call
14
+ # setting a temporary and probably incomplete address to the order
15
+ # only to calculate the available shipping options:
16
+ order.ship_address = address_from_params
17
+
18
+ available_shipping_methods.each_with_object([]) do |(id, rates), options|
19
+ options << shipping_method_data(id, rates)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def available_shipping_methods
26
+ shipments = Spree::Stock::SimpleCoordinator.new(order).shipments
27
+ all_rates = shipments.map(&:shipping_rates).flatten
28
+
29
+ all_rates.group_by(&:shipping_method_id).select do |_, rates|
30
+ rates.size == shipments.size
31
+ end
32
+ end
33
+
34
+ def shipping_method_data(id, rates)
35
+ {
36
+ id: id.to_s,
37
+ label: Spree::ShippingMethod.find(id).name,
38
+ amount: (rates.sum(&:cost) * 100).to_i
39
+ }
40
+ end
41
+
42
+ def address_from_params
43
+ SolidusStripe::AddressFromParamsService.new(shipping_address_params, user).call
44
+ end
45
+ end
46
+ end
@@ -5,7 +5,9 @@ module Spree
5
5
  class StripeCreditCard < Spree::PaymentMethod::CreditCard
6
6
  preference :secret_key, :string
7
7
  preference :publishable_key, :string
8
+ preference :stripe_country, :string
8
9
  preference :v3_elements, :boolean
10
+ preference :v3_intents, :boolean
9
11
 
10
12
  CARD_TYPE_MAPPING = {
11
13
  'American Express' => 'american_express',
@@ -13,6 +15,22 @@ module Spree
13
15
  'Visa' => 'visa'
14
16
  }
15
17
 
18
+ def stripe_config(order)
19
+ {
20
+ id: id,
21
+ publishable_key: preferred_publishable_key
22
+ }.tap do |config|
23
+ config.merge!(
24
+ payment_request: {
25
+ country: preferred_stripe_country,
26
+ currency: order.currency.downcase,
27
+ label: "Payment for order #{order.number}",
28
+ amount: (order.total * 100).to_i
29
+ }
30
+ ) if payment_request?
31
+ end
32
+ end
33
+
16
34
  def partial_name
17
35
  'stripe'
18
36
  end
@@ -21,14 +39,34 @@ module Spree
21
39
  !!preferred_v3_elements
22
40
  end
23
41
 
42
+ def payment_request?
43
+ v3_intents? && preferred_stripe_country.present?
44
+ end
45
+
46
+ def v3_intents?
47
+ !!preferred_v3_intents
48
+ end
49
+
24
50
  def gateway_class
25
- ActiveMerchant::Billing::StripeGateway
51
+ if v3_intents?
52
+ ActiveMerchant::Billing::StripePaymentIntentsGateway
53
+ else
54
+ ActiveMerchant::Billing::StripeGateway
55
+ end
26
56
  end
27
57
 
28
58
  def payment_profiles_supported?
29
59
  true
30
60
  end
31
61
 
62
+ def create_intent(*args)
63
+ gateway.create_intent(*args)
64
+ end
65
+
66
+ def confirm_intent(*args)
67
+ gateway.confirm_intent(*args)
68
+ end
69
+
32
70
  def purchase(money, creditcard, transaction_options)
33
71
  gateway.purchase(*options_for_purchase_or_auth(money, creditcard, transaction_options))
34
72
  end
@@ -63,19 +101,30 @@ module Spree
63
101
 
64
102
  source = update_source!(payment.source)
65
103
  if source.number.blank? && source.gateway_payment_profile_id.present?
66
- creditcard = source.gateway_payment_profile_id
104
+ if v3_intents?
105
+ creditcard = ActiveMerchant::Billing::StripeGateway::StripePaymentToken.new('id' => source.gateway_payment_profile_id)
106
+ else
107
+ creditcard = source.gateway_payment_profile_id
108
+ end
67
109
  else
68
110
  creditcard = source
69
111
  end
70
112
 
71
113
  response = gateway.store(creditcard, options)
72
114
  if response.success?
73
- payment.source.update_attributes!({
74
- cc_type: payment.source.cc_type, # side-effect of update_source!
75
- gateway_customer_profile_id: response.params['id'],
76
- gateway_payment_profile_id: response.params['default_source'] || response.params['default_card']
77
- })
78
-
115
+ if v3_intents?
116
+ payment.source.update!(
117
+ cc_type: payment.source.cc_type,
118
+ gateway_customer_profile_id: response.params['customer'],
119
+ gateway_payment_profile_id: response.params['id']
120
+ )
121
+ else
122
+ payment.source.update!(
123
+ cc_type: payment.source.cc_type,
124
+ gateway_customer_profile_id: response.params['id'],
125
+ gateway_payment_profile_id: response.params['default_source'] || response.params['default_card']
126
+ )
127
+ end
79
128
  else
80
129
  payment.send(:gateway_error, response.message)
81
130
  end
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "bundler/setup"
6
+ require "solidus_stripe"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+ $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"])
11
+
12
+ # (If you use this, don't forget to add pry to your Gemfile!)
13
+ # require "pry"
14
+ # Pry.start
15
+
16
+ require "irb"
17
+ IRB.start(__FILE__)
data/bin/rails CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
- ENGINE_PATH = File.expand_path('../../lib/solidus_stripe/engine', __FILE__)
3
+ # frozen_string_literal: true
5
4
 
6
- require 'rails/all'
7
- require 'rails/engine/commands'
5
+ app_root = 'spec/dummy'
6
+
7
+ unless File.exist? "#{app_root}/bin/rails"
8
+ system "bin/rake", app_root or begin # rubocop:disable Style/AndOr
9
+ warn "Automatic creation of the dummy app failed"
10
+ exit 1
11
+ end
12
+ end
13
+
14
+ Dir.chdir app_root
15
+ exec 'bin/rails', *ARGV
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ gem install bundler --conservative
7
+ bundle update
8
+ bundle exec rake extension:test_app
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ # route to a deprecated controller, will be removed in the future:
3
+ post '/stripe/confirm_payment', to: 'stripe#confirm_payment'
4
+
5
+ # payment intents routes:
6
+ post '/stripe/confirm_intents', to: '/solidus_stripe/intents#confirm'
7
+
8
+ # payment request routes:
9
+ post '/stripe/shipping_rates', to: '/solidus_stripe/payment_request#shipping_rates'
10
+ post '/stripe/update_order', to: '/solidus_stripe/payment_request#update_order'
11
+ end
@@ -4,3 +4,8 @@
4
4
  border: 1px solid #d9d9db;
5
5
  padding: 5px;
6
6
  }
7
+
8
+ #cart #stripe-payment-request {
9
+ clear: both;
10
+ margin-top: 100px;
11
+ }
@@ -3,9 +3,7 @@
3
3
  module SolidusStripe
4
4
  module Generators
5
5
  class InstallGenerator < Rails::Generators::Base
6
- class_option :migrate, type: :boolean, default: true
7
6
  class_option :auto_run_migrations, type: :boolean, default: false
8
- class_option :auto_run_seeds, type: :boolean, default: false
9
7
 
10
8
  def add_stylesheets
11
9
  filename = 'vendor/assets/stylesheets/spree/frontend/all.css'
@@ -19,10 +17,11 @@ module SolidusStripe
19
17
  end
20
18
 
21
19
  def run_migrations
22
- if options.migrate? && running_migrations?
20
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
21
+ if run_migrations
23
22
  run 'bundle exec rake db:migrate'
24
23
  else
25
- puts "Skiping rake db:migrate, don't forget to run it!"
24
+ puts 'Skipping rake db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
26
25
  end
27
26
  end
28
27
 
@@ -32,15 +31,6 @@ module SolidusStripe
32
31
  say_status :loading, 'stripe seed data'
33
32
  rake('db:seed:solidus_stripe')
34
33
  end
35
-
36
- private
37
-
38
- def running_migrations?
39
- options.auto_run_migrations? || begin
40
- response = ask 'Would you like to run the migrations now? [Y/n]'
41
- ['', 'y'].include? response.downcase
42
- end
43
- end
44
34
  end
45
35
  end
46
36
  end
@@ -1,11 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'spree/core'
4
+
3
5
  module SolidusStripe
4
6
  class Engine < Rails::Engine
7
+ include SolidusSupport::EngineExtensions::Decorators
8
+
9
+ isolate_namespace Spree
10
+
5
11
  engine_name 'solidus_stripe'
6
12
 
7
- initializer "spree.payment_method.add_stripe_credit_card", after: "spree.register.payment_methods" do |app|
8
- app.config.spree.payment_methods << Spree::PaymentMethod::StripeCreditCard
13
+ # use rspec for tests
14
+ config.generators do |g|
15
+ g.test_framework :rspec
9
16
  end
10
17
 
11
18
  if SolidusSupport.backend_available?
@@ -14,10 +21,15 @@ module SolidusStripe
14
21
 
15
22
  if SolidusSupport.frontend_available?
16
23
  paths["app/views"] << "lib/views/frontend"
24
+ config.assets.precompile += ['solidus_stripe/stripe-init.js']
17
25
  end
18
26
 
19
27
  if SolidusSupport.api_available?
20
28
  paths["app/views"] << "lib/views/api"
21
29
  end
30
+
31
+ initializer "spree.payment_method.add_stripe_credit_card", after: "spree.register.payment_methods" do |app|
32
+ app.config.spree.payment_methods << Spree::PaymentMethod::StripeCreditCard
33
+ end
22
34
  end
23
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidusStripe
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -1,5 +1,7 @@
1
1
  <% if payment_method.v3_elements? %>
2
2
  <%= render 'spree/checkout/payment/v3/stripe', payment_method: payment_method %>
3
+ <% elsif payment_method.v3_intents? %>
4
+ <%= render 'spree/checkout/payment/v3/intents', payment_method: payment_method %>
3
5
  <% else %>
4
6
  <%= render "spree/checkout/payment/gateway", payment_method: payment_method %>
5
7
  <%= render 'spree/checkout/payment/v2/javascript', payment_method: payment_method %>
@@ -4,7 +4,8 @@
4
4
  </script>
5
5
 
6
6
  <script>
7
- Spree.stripePaymentMethod = $('#payment_method_' + <%= payment_method.id %>);
7
+ window.SolidusStripe = window.SolidusStripe || {};
8
+ SolidusStripe.paymentMethod = $('#payment_method_' + <%= payment_method.id %>);
8
9
  var mapCC, stripeResponseHandler;
9
10
 
10
11
  mapCC = function(ccType) {
@@ -29,25 +30,25 @@
29
30
  $('#stripeError').html(response.error.message);
30
31
  return $('#stripeError').show();
31
32
  } else {
32
- Spree.stripePaymentMethod.find('#card_number, #card_expiry, #card_code').prop("disabled", true);
33
- Spree.stripePaymentMethod.find(".ccType").prop("disabled", false);
34
- Spree.stripePaymentMethod.find(".ccType").val(mapCC(response.card.brand));
33
+ SolidusStripe.paymentMethod.find('#card_number, #card_expiry, #card_code').prop("disabled", true);
34
+ SolidusStripe.paymentMethod.find(".ccType").prop("disabled", false);
35
+ SolidusStripe.paymentMethod.find(".ccType").val(mapCC(response.card.brand));
35
36
  token = response['id'];
36
- paymentMethodId = Spree.stripePaymentMethod.prop('id').split("_")[2];
37
- Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][gateway_payment_profile_id]' value='" + token + "'/>");
38
- Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][last_digits]' value='" + response.card.last4 + "'/>");
39
- Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][month]' value='" + response.card.exp_month + "'/>");
40
- Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][year]' value='" + response.card.exp_year + "'/>");
41
- return Spree.stripePaymentMethod.parents("form").get(0).submit();
37
+ paymentMethodId = SolidusStripe.paymentMethod.prop('id').split("_")[2];
38
+ SolidusStripe.paymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][gateway_payment_profile_id]' value='" + token + "'/>");
39
+ SolidusStripe.paymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][last_digits]' value='" + response.card.last4 + "'/>");
40
+ SolidusStripe.paymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][month]' value='" + response.card.exp_month + "'/>");
41
+ SolidusStripe.paymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId + "][year]' value='" + response.card.exp_year + "'/>");
42
+ return SolidusStripe.paymentMethod.parents("form").get(0).submit();
42
43
  }
43
44
  };
44
45
 
45
46
  $(document).ready(function() {
46
- Spree.stripePaymentMethod.prepend("<div id='stripeError' class='errorExplanation' style='display:none'></div>");
47
+ SolidusStripe.paymentMethod.prepend("<div id='stripeError' class='errorExplanation' style='display:none'></div>");
47
48
  return $('#checkout_form_payment [data-hook=buttons]').click(function() {
48
49
  var expiration, params;
49
50
  $('#stripeError').hide();
50
- if (Spree.stripePaymentMethod.is(':visible')) {
51
+ if (SolidusStripe.paymentMethod.is(':visible')) {
51
52
  expiration = $('.cardExpiry:visible').payment('cardExpiryVal');
52
53
  params = $.extend({
53
54
  number: $('.cardNumber:visible').val(),
@@ -0,0 +1,28 @@
1
+ <script>
2
+ // Stripe V3 elements specific JS code
3
+
4
+ var cardNumber = initElements();
5
+
6
+ cardNumber.addEventListener('change', function(event) {
7
+ if (event.error) {
8
+ showError(event.error.message);
9
+ } else {
10
+ errorElement.hide().text('');
11
+ }
12
+ });
13
+
14
+ form.bind('submit', function(event) {
15
+ if (element.is(':visible')) {
16
+ event.preventDefault();
17
+
18
+ stripe.createToken(cardNumber).then(function(result) {
19
+ if (result.error) {
20
+ showError(result.error.message);
21
+ } else {
22
+ stripeTokenHandler(result.token);
23
+ form[0].submit();
24
+ }
25
+ });
26
+ }
27
+ });
28
+ </script>
@@ -0,0 +1,42 @@
1
+ <div id="payment-request-button" data-stripe-config="<%= payment_method.stripe_config(current_order).to_json %>"></div>
2
+
3
+ <%= image_tag 'credit_cards/credit_card.gif', id: 'credit-card-image' %>
4
+ <% param_prefix = "payment_source[#{payment_method.id}]" %>
5
+
6
+ <div class="field field-required">
7
+ <%= label_tag "name_on_card_#{payment_method.id}", t('spree.name_on_card') %>
8
+ <%= text_field_tag "#{param_prefix}[name]", "#{@order.billing_firstname} #{@order.billing_lastname}", { id: "name_on_card_#{payment_method.id}", autocomplete: "cc-name" } %>
9
+ </div>
10
+
11
+ <div class="field field-required" data-hook="card_number">
12
+ <%= label_tag "card_number", t('spree.card_number') %>
13
+ <div id="card_number"></div>
14
+ <span id="card_type" style="display:none;">
15
+ ( <span id="looks_like" ><%= t('spree.card_type_is') %> <span id="type"></span></span>
16
+ <span id="unrecognized"><%= t('spree.unrecognized_card_type') %></span>
17
+ )
18
+ </span>
19
+ </div>
20
+
21
+ <div class="field field-required" data-hook="card_expiration">
22
+ <%= label_tag "card_expiry", t('spree.expiration') %>
23
+ <div id="card_expiry"></div>
24
+ </div>
25
+
26
+ <div class="field field-required" data-hook="card_code">
27
+ <%= label_tag "card_cvc", t('spree.card_code') %>
28
+ <div id="card_cvc"></div>
29
+ <%= link_to "(#{t('spree.what_is_this')})", spree.cvv_path, target: '_blank', "data-hook" => "cvv_link", id: "cvv_link" %>
30
+ </div>
31
+
32
+ <div id="card-errors" class='errorExplanation' role="alert" style="display: none"></div>
33
+
34
+ <% if @order.bill_address %>
35
+ <%= fields_for "#{param_prefix}[address_attributes]", @order.bill_address do |f| %>
36
+ <%= render partial: 'spree/address/form_hidden', locals: { form: f } %>
37
+ <% end %>
38
+ <% end %>
39
+
40
+ <%= hidden_field_tag "#{param_prefix}[cc_type]", '', id: "cc_type", class: 'ccType' %>
41
+
42
+ <script src="https://js.stripe.com/v3/"></script>