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
data/README.md CHANGED
@@ -13,7 +13,7 @@ Installation
13
13
  In your Gemfile:
14
14
 
15
15
  ```ruby
16
- gem "solidus_stripe", github: "solidusio/solidus_stripe"
16
+ gem 'solidus_stripe', '~> 1.0.0'
17
17
  ```
18
18
 
19
19
  Then run from the command line:
@@ -54,7 +54,9 @@ Spree.config do |config|
54
54
  'stripe_env_credentials',
55
55
  secret_key: ENV['STRIPE_SECRET_KEY'],
56
56
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
57
+ stripe_country: 'US',
57
58
  v3_elements: false,
59
+ v3_intents: false,
58
60
  server: Rails.env.production? ? 'production' : 'test',
59
61
  test_mode: !Rails.env.production?
60
62
  )
@@ -67,6 +69,61 @@ your application will start using the static configuration to process
67
69
  Stripe payments.
68
70
 
69
71
 
72
+ Using Stripe Payment Intents API
73
+ --------------------------------
74
+
75
+ If you want to use the new SCA-ready Stripe Payment Intents API you need
76
+ to change the `v3_intents` preference from the code above to true and,
77
+ if you want to allow also Apple Pay and Google Pay payments, set the
78
+ `stripe_country` preference, which represents the two-letter country
79
+ code of your Stripe account:
80
+
81
+
82
+ ```ruby
83
+ Spree.config do |config|
84
+ # ...
85
+
86
+ config.static_model_preferences.add(
87
+ Spree::PaymentMethod::StripeCreditCard,
88
+ 'stripe_env_credentials',
89
+ secret_key: ENV['STRIPE_SECRET_KEY'],
90
+ publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
91
+ stripe_country: 'US',
92
+ v3_elements: false,
93
+ v3_intents: true,
94
+ server: Rails.env.production? ? 'production' : 'test',
95
+ test_mode: !Rails.env.production?
96
+ )
97
+ end
98
+ ```
99
+
100
+ Apple Pay and Google Pay
101
+ -----------------------
102
+
103
+ The Payment Intents API now supports also Apple Pay and Google Pay via
104
+ the [payment request button API](https://stripe.com/docs/stripe-js/elements/payment-request-button).
105
+ Check the Payment Intents section for setup details. Also, please
106
+ refer to the official Stripe documentation for configuring your
107
+ Stripe account to receive payments via Apple Pay.
108
+
109
+ It's possible to pay with Apple Pay and Google Pay directly from the cart
110
+ page. The functionality is self-contained in the view partial
111
+ `_stripe_payment_request_button.html.erb`. In order to use it, you need
112
+ to load that partial in the `orders#edit` frontend page, and pass it the
113
+ payment method configured for Stripe via the local variable
114
+ `cart_checkout_payment_method`, for example using `deface`:
115
+
116
+ ```ruby
117
+ # app/overrides/spree/orders/edit/add_payment_request_button.html.erb.deface
118
+
119
+ <!-- insert_after '[data-hook="cart_container"]' -->
120
+ <%= render 'stripe_payment_request_button', cart_checkout_payment_method: Spree::PaymentMethod::StripeCreditCard.first %>
121
+ ```
122
+
123
+ Of course, rules stated in the paragraph above (remember to add the stripe country
124
+ config value, for example) apply also for this payment method.
125
+
126
+
70
127
  Migrating from solidus_gateway
71
128
  ------------------------------
72
129
 
data/Rakefile CHANGED
@@ -1,23 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler'
4
- Bundler::GemHelper.install_tasks
3
+ require 'solidus_dev_support/rake_tasks'
4
+ SolidusDevSupport::RakeTasks.install
5
5
 
6
- require 'rspec/core/rake_task'
7
- require 'spree/testing_support/common_rake'
8
-
9
- RSpec::Core::RakeTask.new
10
-
11
- task :default do
12
- if Dir["spec/dummy"].empty?
13
- Rake::Task[:test_app].invoke
14
- Dir.chdir("../../")
15
- end
16
- Rake::Task[:spec].invoke
17
- end
18
-
19
- desc "Generates a dummy app for testing"
20
- task :test_app do
21
- ENV['LIB_NAME'] = 'solidus_stripe'
22
- Rake::Task['common:test_app'].invoke
23
- end
6
+ task default: %w[extension:test_app extension:specs]
@@ -0,0 +1 @@
1
+ //= require ./stripe-init/base
@@ -0,0 +1,180 @@
1
+ window.SolidusStripe = window.SolidusStripe || {};
2
+
3
+ SolidusStripe.paymentMethod = {
4
+ config: $('[data-stripe-config').data('stripe-config'),
5
+ requestShipping: false
6
+ }
7
+
8
+ var authToken = $('meta[name="csrf-token"]').attr('content');
9
+
10
+ var stripe = Stripe(SolidusStripe.paymentMethod.config.publishable_key)
11
+ var elements = stripe.elements({locale: 'en'});
12
+
13
+ var element = $('#payment_method_' + SolidusStripe.paymentMethod.config.id);
14
+ var form = element.parents('form');
15
+ var errorElement = form.find('#card-errors');
16
+ var submitButton = form.find('input[type="submit"]');
17
+
18
+ function stripeTokenHandler(token) {
19
+ var baseSelector = `<input type='hidden' class='stripeToken' name='payment_source[${SolidusStripe.paymentMethod.config.id}]`;
20
+
21
+ element.append(`${baseSelector}[gateway_payment_profile_id]' value='${token.id}'/>`);
22
+ element.append(`${baseSelector}[last_digits]' value='${token.card.last4}'/>`);
23
+ element.append(`${baseSelector}[month]' value='${token.card.exp_month}'/>`);
24
+ element.append(`${baseSelector}[year]' value='${token.card.exp_year}'/>`);
25
+ form.find('input#cc_type').val(mapCC(token.card.brand || token.card.type));
26
+ };
27
+
28
+ function initElements() {
29
+ var style = {
30
+ base: {
31
+ color: 'black',
32
+ fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
33
+ fontSmoothing: 'antialiased',
34
+ fontSize: '14px',
35
+ '::placeholder': {
36
+ color: 'silver'
37
+ }
38
+ },
39
+ invalid: {
40
+ color: 'red',
41
+ iconColor: 'red'
42
+ }
43
+ };
44
+
45
+ elements.create('cardExpiry', {style: style}).mount('#card_expiry');
46
+ elements.create('cardCvc', {style: style}).mount('#card_cvc');
47
+
48
+ var cardNumber = elements.create('cardNumber', {style: style});
49
+ cardNumber.mount('#card_number');
50
+
51
+ return cardNumber;
52
+ }
53
+
54
+ function setUpPaymentRequest(config, onPrButtonMounted) {
55
+ if (typeof config !== 'undefined') {
56
+ var paymentRequest = stripe.paymentRequest({
57
+ country: config.country,
58
+ currency: config.currency,
59
+ total: {
60
+ label: config.label,
61
+ amount: config.amount
62
+ },
63
+ requestPayerName: true,
64
+ requestPayerEmail: true,
65
+ requestShipping: config.requestShipping,
66
+ shippingOptions: [
67
+ ]
68
+ });
69
+
70
+ var prButton = elements.create('paymentRequestButton', {
71
+ paymentRequest: paymentRequest
72
+ });
73
+
74
+ paymentRequest.canMakePayment().then(function(result) {
75
+ var id = 'payment-request-button';
76
+
77
+ if (result) {
78
+ prButton.mount('#' + id);
79
+ } else {
80
+ document.getElementById(id).style.display = 'none';
81
+ }
82
+ if (typeof onPrButtonMounted === 'function') {
83
+ onPrButtonMounted(id, result);
84
+ }
85
+ });
86
+
87
+ paymentRequest.on('paymentmethod', function(result) {
88
+ errorElement.text('').hide();
89
+ handlePayment(result);
90
+ });
91
+
92
+ paymentRequest.on('shippingaddresschange', function(ev) {
93
+ fetch('/stripe/shipping_rates', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({
97
+ authenticity_token: authToken,
98
+ shipping_address: ev.shippingAddress
99
+ })
100
+ }).then(function(response) {
101
+ return response.json();
102
+ }).then(function(result) {
103
+ if (result.error) {
104
+ showError(result.error);
105
+ return false;
106
+ } else {
107
+ ev.updateWith({
108
+ status: 'success',
109
+ shippingOptions: result.shipping_rates
110
+ });
111
+ }
112
+ });
113
+ });
114
+
115
+ return paymentRequest;
116
+ }
117
+ };
118
+
119
+ function handleServerResponse(response, payment) {
120
+ if (response.error) {
121
+ showError(response.error);
122
+ completePaymentRequest(payment, 'fail');
123
+ } else if (response.requires_action) {
124
+ stripe.handleCardAction(
125
+ response.stripe_payment_intent_client_secret
126
+ ).then(function(result) {
127
+ if (result.error) {
128
+ showError(result.error.message);
129
+ } else {
130
+ fetch('/stripe/confirm_intents', {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({
134
+ spree_payment_method_id: SolidusStripe.paymentMethod.config.id,
135
+ stripe_payment_intent_id: result.paymentIntent.id,
136
+ authenticity_token: authToken
137
+ })
138
+ }).then(function(confirmResult) {
139
+ return confirmResult.json();
140
+ }).then(handleServerResponse);
141
+ }
142
+ });
143
+ } else {
144
+ completePaymentRequest(payment, 'success');
145
+ submitPayment(payment);
146
+ }
147
+ }
148
+
149
+ function completePaymentRequest(payment, state) {
150
+ if (payment && typeof payment.complete === 'function') {
151
+ payment.complete(state);
152
+ }
153
+ }
154
+
155
+ function showError(error) {
156
+ errorElement.text(error).show();
157
+
158
+ if (submitButton.length) {
159
+ setTimeout(function() {
160
+ $.rails.enableElement(submitButton[0]);
161
+ submitButton.removeAttr('disabled').removeClass('disabled');
162
+ }, 100);
163
+ }
164
+ };
165
+
166
+ function mapCC(ccType) {
167
+ if (ccType === 'MasterCard') {
168
+ return 'mastercard';
169
+ } else if (ccType === 'Visa') {
170
+ return 'visa';
171
+ } else if (ccType === 'American Express') {
172
+ return 'amex';
173
+ } else if (ccType === 'Discover') {
174
+ return 'discover';
175
+ } else if (ccType === 'Diners Club') {
176
+ return 'dinersclub';
177
+ } else if (ccType === 'JCB') {
178
+ return 'jcb';
179
+ }
180
+ };
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class IntentsController < Spree::BaseController
5
+ include Spree::Core::ControllerHelpers::Order
6
+
7
+ def confirm
8
+ begin
9
+ if params[:stripe_payment_method_id].present?
10
+ intent = stripe.create_intent(
11
+ (current_order.total * 100).to_i,
12
+ params[:stripe_payment_method_id],
13
+ currency: current_order.currency,
14
+ confirmation_method: 'manual',
15
+ confirm: true,
16
+ setup_future_usage: 'on_session',
17
+ metadata: { order_id: current_order.id }
18
+ )
19
+ elsif params[:stripe_payment_intent_id].present?
20
+ intent = stripe.confirm_intent(params[:stripe_payment_intent_id], nil)
21
+ end
22
+ rescue Stripe::CardError => e
23
+ render json: { error: e.message }, status: 500
24
+ return
25
+ end
26
+
27
+ generate_payment_response(intent)
28
+ end
29
+
30
+ private
31
+
32
+ def stripe
33
+ @stripe ||= Spree::PaymentMethod::StripeCreditCard.find(params[:spree_payment_method_id])
34
+ end
35
+
36
+ def generate_payment_response(intent)
37
+ response = intent.params
38
+ # Note that if your API version is before 2019-02-11, 'requires_action'
39
+ # appears as 'requires_source_action'.
40
+ if %w[requires_source_action requires_action].include?(response['status']) && response['next_action']['type'] == 'use_stripe_sdk'
41
+ render json: {
42
+ requires_action: true,
43
+ stripe_payment_intent_client_secret: response['client_secret']
44
+ }
45
+ elsif response['status'] == 'succeeded'
46
+ render json: { success: true }
47
+ else
48
+ render json: { error: response['error']['message'] }, status: 500
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class PaymentRequestController < Spree::BaseController
5
+ include Spree::Core::ControllerHelpers::Order
6
+
7
+ def shipping_rates
8
+ rates = SolidusStripe::ShippingRatesService.new(
9
+ current_order,
10
+ spree_current_user,
11
+ params[:shipping_address]
12
+ ).call
13
+
14
+ if rates.any?
15
+ render json: { success: true, shipping_rates: rates }
16
+ else
17
+ render json: { success: false, error: 'No shipping method available for that address' }, status: 500
18
+ end
19
+ end
20
+
21
+ def update_order
22
+ current_order.restart_checkout_flow
23
+
24
+ address = SolidusStripe::AddressFromParamsService.new(
25
+ params[:shipping_address],
26
+ spree_current_user
27
+ ).call
28
+
29
+ if address.valid?
30
+ SolidusStripe::PrepareOrderForPaymentService.new(address, self).call
31
+
32
+ if current_order.payment?
33
+ render json: { success: true }
34
+ else
35
+ render json: { success: false, error: 'Order not ready for payment. Try manual checkout.' }, status: 500
36
+ end
37
+ else
38
+ render json: { success: false, error: address.errors.full_messages.to_sentence }, status: 500
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class StripeController < SolidusStripe::IntentsController
5
+ include Core::ControllerHelpers::Order
6
+
7
+ def confirm_payment
8
+ Deprecation.warn "please use SolidusStripe::IntentsController#confirm"
9
+
10
+ confirm
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class AddressFromParamsService
5
+ attr_reader :address_params, :user
6
+
7
+ def initialize(address_params, user)
8
+ @address_params, @user = address_params, user
9
+ end
10
+
11
+ def call
12
+ if user
13
+ user.addresses.find_or_initialize_by(attributes)
14
+ else
15
+ Spree::Address.new(attributes)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def attributes
22
+ @attributes ||= begin
23
+ default_attributes.tap do |attributes|
24
+ # possibly anonymized attributes:
25
+ phone = address_params[:phone]
26
+ lines = address_params[:addressLine]
27
+ names = address_params[:recipient].split(' ')
28
+
29
+ attributes.merge!(
30
+ state_id: state&.id,
31
+ firstname: names.first,
32
+ lastname: names.last,
33
+ phone: phone,
34
+ address1: lines.first,
35
+ address2: lines.second
36
+ ).reject! { |_, value| value.blank? }
37
+ end
38
+ end
39
+ end
40
+
41
+ def country
42
+ @country ||= Spree::Country.find_by_iso(address_params[:country])
43
+ end
44
+
45
+ def state
46
+ @state ||= country.states.find_by_abbr(address_params[:region])
47
+ end
48
+
49
+ def default_attributes
50
+ {
51
+ country_id: country.id,
52
+ city: address_params[:city],
53
+ zipcode: address_params[:postalCode]
54
+ }
55
+ end
56
+ end
57
+ end