solidus_stripe 2.0.0 → 3.2.1

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