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
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