solidus_stripe 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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>