solidus_stripe 2.0.0 → 3.2.1

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/README.md +131 -7
  4. data/app/assets/javascripts/spree/frontend/solidus_stripe.js +6 -0
  5. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +88 -0
  6. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +148 -0
  7. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +20 -0
  8. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +84 -0
  9. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +123 -0
  10. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +16 -0
  11. data/app/controllers/solidus_stripe/intents_controller.rb +29 -20
  12. data/app/decorators/models/spree/order_update_attributes_decorator.rb +39 -0
  13. data/app/decorators/models/spree/payment_decorator.rb +11 -0
  14. data/app/models/solidus_stripe/create_intents_order_service.rb +70 -0
  15. data/app/models/spree/payment_method/stripe_credit_card.rb +4 -9
  16. data/lib/generators/solidus_stripe/install/install_generator.rb +2 -5
  17. data/lib/solidus_stripe/engine.rb +0 -1
  18. data/lib/solidus_stripe/version.rb +1 -1
  19. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +1 -1
  20. data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +1 -0
  21. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +1 -3
  22. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +1 -5
  23. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +2 -5
  24. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +1 -79
  25. data/solidus_stripe.gemspec +1 -1
  26. data/spec/features/stripe_checkout_spec.rb +143 -68
  27. data/spec/spec_helper.rb +1 -0
  28. data/spec/support/solidus_address_helper.rb +15 -0
  29. metadata +17 -8
  30. data/app/assets/javascripts/solidus_stripe/stripe-init.js +0 -1
  31. data/app/assets/javascripts/solidus_stripe/stripe-init/base.js +0 -180
  32. data/lib/views/frontend/spree/checkout/payment/v3/_elements_js.html.erb +0 -28
  33. data/lib/views/frontend/spree/checkout/payment/v3/_intents_js.html.erb +0 -48
@@ -0,0 +1,20 @@
1
+ $(function() {
2
+ var stripeV3Api = $('[data-v3-api]').data('v3-api');
3
+
4
+ if (stripeV3Api) {
5
+ $.getScript('https://js.stripe.com/v3/')
6
+ .done(function() {
7
+ switch (stripeV3Api) {
8
+ case 'elements':
9
+ new SolidusStripe.Elements().init();
10
+ break;
11
+ case 'payment-intents':
12
+ new SolidusStripe.PaymentIntents().init();
13
+ break;
14
+ case 'payment-request-button':
15
+ new SolidusStripe.CartPageCheckout().init();
16
+ break;
17
+ }
18
+ });
19
+ }
20
+ });
@@ -0,0 +1,84 @@
1
+ SolidusStripe.PaymentIntents = function() {
2
+ SolidusStripe.Elements.call(this);
3
+ };
4
+
5
+ SolidusStripe.PaymentIntents.prototype = Object.create(SolidusStripe.Elements.prototype);
6
+ Object.defineProperty(SolidusStripe.PaymentIntents.prototype, 'constructor', {
7
+ value: SolidusStripe.PaymentIntents,
8
+ enumerable: false,
9
+ writable: true
10
+ });
11
+
12
+ SolidusStripe.PaymentIntents.prototype.init = function() {
13
+ this.setUpPaymentRequest();
14
+ this.initElements();
15
+ };
16
+
17
+ SolidusStripe.PaymentIntents.prototype.onPrPayment = function(payment) {
18
+ if (payment.error) {
19
+ this.showError(payment.error.message);
20
+ } else {
21
+ var that = this;
22
+
23
+ this.elementsTokenHandler(payment.paymentMethod);
24
+ fetch('/stripe/confirm_intents', {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json'
28
+ },
29
+ body: JSON.stringify({
30
+ form_data: this.form.serialize(),
31
+ spree_payment_method_id: this.config.id,
32
+ stripe_payment_method_id: payment.paymentMethod.id,
33
+ authenticity_token: this.authToken
34
+ })
35
+ }).then(function(response) {
36
+ response.json().then(function(json) {
37
+ that.handleServerResponse(json, payment);
38
+ })
39
+ });
40
+ }
41
+ };
42
+
43
+ SolidusStripe.PaymentIntents.prototype.onFormSubmit = function(event) {
44
+ if (this.element.is(':visible')) {
45
+ event.preventDefault();
46
+
47
+ this.errorElement.text('').hide();
48
+
49
+ this.stripe.createPaymentMethod(
50
+ 'card',
51
+ this.cardNumber
52
+ ).then(this.onIntentsPayment.bind(this));
53
+ }
54
+ };
55
+
56
+ SolidusStripe.PaymentIntents.prototype.submitPayment = function(_payment) {
57
+ this.form.unbind('submit').submit();
58
+ };
59
+
60
+ SolidusStripe.PaymentIntents.prototype.onIntentsPayment = function(payment) {
61
+ if (payment.error) {
62
+ this.showError(payment.error.message);
63
+ } else {
64
+ var that = this;
65
+
66
+ this.elementsTokenHandler(payment.paymentMethod);
67
+ fetch('/stripe/confirm_intents', {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json'
71
+ },
72
+ body: JSON.stringify({
73
+ form_data: this.form.serialize(),
74
+ spree_payment_method_id: this.config.id,
75
+ stripe_payment_method_id: payment.paymentMethod.id,
76
+ authenticity_token: this.authToken
77
+ })
78
+ }).then(function(response) {
79
+ response.json().then(function(json) {
80
+ that.handleServerResponse(json, payment);
81
+ })
82
+ });
83
+ }
84
+ };
@@ -0,0 +1,123 @@
1
+ // Shared code between Payment Intents and Payment Request Button on cart page
2
+
3
+ (function() {
4
+ var PaymentRequestButtonShared;
5
+
6
+ PaymentRequestButtonShared = {
7
+ authToken: $('meta[name="csrf-token"]').attr('content'),
8
+
9
+ setUpPaymentRequest: function(opts) {
10
+ var opts = opts || {};
11
+ var config = this.config.payment_request;
12
+
13
+ if (config) {
14
+ config.requestShipping = opts.requestShipping || false;
15
+
16
+ var paymentRequest = this.stripe.paymentRequest({
17
+ country: config.country,
18
+ currency: config.currency,
19
+ total: {
20
+ label: config.label,
21
+ amount: config.amount
22
+ },
23
+ requestPayerName: true,
24
+ requestPayerEmail: true,
25
+ requestShipping: config.requestShipping,
26
+ shippingOptions: []
27
+ });
28
+
29
+ var prButton = this.elements.create('paymentRequestButton', {
30
+ paymentRequest: paymentRequest
31
+ });
32
+
33
+ var onButtonMount = function(result) {
34
+ var id = 'payment-request-button';
35
+
36
+ if (result) {
37
+ prButton.mount('#' + id);
38
+ } else {
39
+ document.getElementById(id).style.display = 'none';
40
+ }
41
+ if (typeof this.onPrButtonMounted === 'function') {
42
+ this.onPrButtonMounted(id, result);
43
+ }
44
+ }
45
+ paymentRequest.canMakePayment().then(onButtonMount.bind(this));
46
+
47
+ var onPrPaymentMethod = function(result) {
48
+ this.errorElement.text('').hide();
49
+ this.onPrPayment(result);
50
+ };
51
+ paymentRequest.on('paymentmethod', onPrPaymentMethod.bind(this));
52
+
53
+ onShippingAddressChange = function(ev) {
54
+ var showError = this.showError.bind(this);
55
+
56
+ fetch('/stripe/shipping_rates', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({
60
+ authenticity_token: this.authToken,
61
+ shipping_address: ev.shippingAddress
62
+ })
63
+ }).then(function(response) {
64
+ return response.json();
65
+ }).then(function(result) {
66
+ if (result.error) {
67
+ showError(result.error);
68
+ return false;
69
+ } else {
70
+ ev.updateWith({
71
+ status: 'success',
72
+ shippingOptions: result.shipping_rates
73
+ });
74
+ }
75
+ });
76
+ };
77
+ paymentRequest.on('shippingaddresschange', onShippingAddressChange.bind(this));
78
+ }
79
+ },
80
+
81
+ handleServerResponse: function(response, payment) {
82
+ if (response.error) {
83
+ this.showError(response.error);
84
+ this.completePaymentRequest(payment, 'fail');
85
+ } else if (response.requires_action) {
86
+ this.stripe.handleCardAction(
87
+ response.stripe_payment_intent_client_secret
88
+ ).then(this.onIntentsClientSecret.bind(this));
89
+ } else {
90
+ this.completePaymentRequest(payment, 'success');
91
+ this.submitPayment(payment);
92
+ }
93
+ },
94
+
95
+ onIntentsClientSecret: function(result) {
96
+ if (result.error) {
97
+ this.showError(result.error);
98
+ } else {
99
+ fetch('/stripe/confirm_intents', {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({
103
+ form_data: this.form.serialize(),
104
+ spree_payment_method_id: this.config.id,
105
+ stripe_payment_intent_id: result.paymentIntent.id,
106
+ authenticity_token: this.authToken
107
+ })
108
+ }).then(function(confirmResult) {
109
+ return confirmResult.json();
110
+ }).then(this.handleServerResponse.bind(this));
111
+ }
112
+ },
113
+
114
+ completePaymentRequest: function(payment, state) {
115
+ if (payment && typeof payment.complete === 'function') {
116
+ payment.complete(state);
117
+ }
118
+ }
119
+ };
120
+
121
+ Object.assign(SolidusStripe.PaymentIntents.prototype, PaymentRequestButtonShared);
122
+ Object.assign(SolidusStripe.CartPageCheckout.prototype, PaymentRequestButtonShared);
123
+ })()
@@ -0,0 +1,16 @@
1
+ window.SolidusStripe = window.SolidusStripe || {};
2
+
3
+ SolidusStripe.Payment = function() {
4
+ this.config = $('[data-stripe-config]').data('stripe-config');
5
+ this.element = $('#payment_method_' + this.config.id);
6
+ this.authToken = $('meta[name="csrf-token"]').attr('content');
7
+
8
+ this.stripe = Stripe(this.config.publishable_key);
9
+ this.elements = this.stripe.elements(this.elementsBaseOptions());
10
+ };
11
+
12
+ SolidusStripe.Payment.prototype.elementsBaseOptions = function () {
13
+ return {
14
+ locale: 'en'
15
+ };
16
+ };
@@ -6,25 +6,19 @@ module SolidusStripe
6
6
 
7
7
  def confirm
8
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)
9
+ @intent = begin
10
+ if params[:stripe_payment_method_id].present?
11
+ create_intent
12
+ elsif params[:stripe_payment_intent_id].present?
13
+ stripe.confirm_intent(params[:stripe_payment_intent_id], nil)
14
+ end
21
15
  end
22
16
  rescue Stripe::CardError => e
23
17
  render json: { error: e.message }, status: 500
24
18
  return
25
19
  end
26
20
 
27
- generate_payment_response(intent)
21
+ generate_payment_response
28
22
  end
29
23
 
30
24
  private
@@ -33,20 +27,35 @@ module SolidusStripe
33
27
  @stripe ||= Spree::PaymentMethod::StripeCreditCard.find(params[:spree_payment_method_id])
34
28
  end
35
29
 
36
- def generate_payment_response(intent)
37
- response = intent.params
30
+ def generate_payment_response
31
+ response = @intent.params
38
32
  # Note that if your API version is before 2019-02-11, 'requires_action'
39
33
  # appears as 'requires_source_action'.
40
34
  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'
35
+ render json: {
36
+ requires_action: true,
37
+ stripe_payment_intent_client_secret: response['client_secret']
38
+ }
39
+ elsif response['status'] == 'requires_capture'
40
+ SolidusStripe::CreateIntentsOrderService.new(@intent, stripe, self).call
46
41
  render json: { success: true }
47
42
  else
48
43
  render json: { error: response['error']['message'] }, status: 500
49
44
  end
50
45
  end
46
+
47
+ def create_intent
48
+ stripe.create_intent(
49
+ (current_order.total * 100).to_i,
50
+ params[:stripe_payment_method_id],
51
+ description: "Solidus Order ID: #{current_order.number} (pending)",
52
+ currency: current_order.currency,
53
+ confirmation_method: 'manual',
54
+ capture_method: 'manual',
55
+ confirm: true,
56
+ setup_future_usage: 'off_session',
57
+ metadata: { order_id: current_order.id }
58
+ )
59
+ end
51
60
  end
52
61
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module OrderUpdateAttributesDecorator
5
+ def assign_payments_attributes
6
+ return if payments_attributes.empty?
7
+ return if adding_new_stripe_payment_intents_card?
8
+
9
+ stripe_intents_pending_payments.each(&:void_transaction!)
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def adding_new_stripe_payment_intents_card?
17
+ paying_with_stripe_intents? && stripe_intents_pending_payments.any?
18
+ end
19
+
20
+ def stripe_intents_pending_payments
21
+ @stripe_intents_pending_payments ||= order.payments.valid.select do |payment|
22
+ payment_method = payment.payment_method
23
+ payment.pending? && stripe_intents?(payment_method)
24
+ end
25
+ end
26
+
27
+ def paying_with_stripe_intents?
28
+ if id = payments_attributes.first&.dig(:payment_method_id)
29
+ stripe_intents?(Spree::PaymentMethod.find(id))
30
+ end
31
+ end
32
+
33
+ def stripe_intents?(payment_method)
34
+ payment_method.respond_to?(:v3_intents?) && payment_method.v3_intents?
35
+ end
36
+
37
+ ::Spree::OrderUpdateAttributes.prepend(self)
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module PaymentDecorator
5
+ def gateway_order_identifier
6
+ gateway_order_id
7
+ end
8
+
9
+ ::Spree::Payment.prepend(self)
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe
4
+ class CreateIntentsOrderService
5
+ attr_reader :intent, :stripe, :controller
6
+
7
+ delegate :request, :current_order, :params, to: :controller
8
+
9
+ def initialize(intent, stripe, controller)
10
+ @intent, @stripe, @controller = intent, stripe, controller
11
+ end
12
+
13
+ def call
14
+ invalidate_previous_payment_intents_payments
15
+ payment = create_payment
16
+ description = "Solidus Order ID: #{payment.gateway_order_identifier}"
17
+ stripe.update_intent(nil, response['id'], nil, description: description)
18
+ end
19
+
20
+ private
21
+
22
+ def invalidate_previous_payment_intents_payments
23
+ if stripe.v3_intents?
24
+ current_order.payments.pending.where(payment_method: stripe).each(&:void_transaction!)
25
+ end
26
+ end
27
+
28
+ def create_payment
29
+ Spree::OrderUpdateAttributes.new(
30
+ current_order,
31
+ payment_params,
32
+ request_env: request.headers.env
33
+ ).apply
34
+
35
+ Spree::Payment.find_by(response_code: response['id']).tap do |payment|
36
+ payment.update!(state: :pending)
37
+ end
38
+ end
39
+
40
+ def payment_params
41
+ card = response['charges']['data'][0]['payment_method_details']['card']
42
+ address_attributes = form_data['payment_source'][stripe.id.to_s]['address_attributes']
43
+
44
+ {
45
+ payments_attributes: [{
46
+ payment_method_id: stripe.id,
47
+ amount: current_order.total,
48
+ response_code: response['id'],
49
+ source_attributes: {
50
+ month: card['exp_month'],
51
+ year: card['exp_year'],
52
+ cc_type: card['brand'],
53
+ gateway_payment_profile_id: response['payment_method'],
54
+ last_digits: card['last4'],
55
+ name: current_order.bill_address.full_name,
56
+ address_attributes: address_attributes
57
+ }
58
+ }]
59
+ }
60
+ end
61
+
62
+ def response
63
+ intent.params
64
+ end
65
+
66
+ def form_data
67
+ Rack::Utils.parse_nested_query(params[:form_data])
68
+ end
69
+ end
70
+ end
@@ -15,6 +15,8 @@ module Spree
15
15
  'Visa' => 'visa'
16
16
  }
17
17
 
18
+ delegate :create_intent, :update_intent, :confirm_intent, to: :gateway
19
+
18
20
  def stripe_config(order)
19
21
  {
20
22
  id: id,
@@ -59,14 +61,6 @@ module Spree
59
61
  true
60
62
  end
61
63
 
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
-
70
64
  def purchase(money, creditcard, transaction_options)
71
65
  gateway.purchase(*options_for_purchase_or_auth(money, creditcard, transaction_options))
72
66
  end
@@ -140,8 +134,9 @@ module Spree
140
134
 
141
135
  def options_for_purchase_or_auth(money, creditcard, transaction_options)
142
136
  options = {}
143
- options[:description] = "Spree Order ID: #{transaction_options[:order_id]}"
137
+ options[:description] = "Solidus Order ID: #{transaction_options[:order_id]}"
144
138
  options[:currency] = transaction_options[:currency]
139
+ options[:off_session] = true if v3_intents?
145
140
 
146
141
  if customer = creditcard.gateway_customer_profile_id
147
142
  options[:customer] = customer