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.
- checksums.yaml +4 -4
- data/.gem_release.yml +2 -5
- data/.github/stale.yml +17 -0
- data/.gitignore +11 -4
- data/.rspec +2 -1
- data/.rubocop.yml +2 -319
- data/Gemfile +16 -9
- data/LICENSE +26 -0
- data/README.md +58 -1
- data/Rakefile +3 -20
- data/app/assets/javascripts/solidus_stripe/stripe-init.js +1 -0
- data/app/assets/javascripts/solidus_stripe/stripe-init/base.js +180 -0
- data/app/controllers/solidus_stripe/intents_controller.rb +52 -0
- data/app/controllers/solidus_stripe/payment_request_controller.rb +42 -0
- data/app/controllers/spree/stripe_controller.rb +13 -0
- data/app/models/solidus_stripe/address_from_params_service.rb +57 -0
- data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +46 -0
- data/app/models/solidus_stripe/shipping_rates_service.rb +46 -0
- data/app/models/spree/payment_method/stripe_credit_card.rb +57 -8
- data/bin/console +17 -0
- data/bin/rails +12 -4
- data/bin/setup +8 -0
- data/config/routes.rb +11 -0
- data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +5 -0
- data/lib/generators/solidus_stripe/install/install_generator.rb +3 -13
- data/lib/solidus_stripe/engine.rb +14 -2
- data/lib/solidus_stripe/version.rb +1 -1
- data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +2 -0
- data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +13 -12
- data/lib/views/frontend/spree/checkout/payment/v3/_elements_js.html.erb +28 -0
- data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +42 -0
- data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +5 -0
- data/lib/views/frontend/spree/checkout/payment/v3/_intents_js.html.erb +48 -0
- data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +3 -131
- data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +92 -0
- data/solidus_stripe.gemspec +15 -20
- data/spec/features/stripe_checkout_spec.rb +196 -35
- data/spec/models/solidus_stripe/address_from_params_service_spec.rb +62 -0
- data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +65 -0
- data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +54 -0
- data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +44 -5
- data/spec/spec_helper.rb +9 -7
- metadata +38 -136
data/README.md
CHANGED
@@ -13,7 +13,7 @@ Installation
|
|
13
13
|
In your Gemfile:
|
14
14
|
|
15
15
|
```ruby
|
16
|
-
gem
|
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 '
|
4
|
-
|
3
|
+
require 'solidus_dev_support/rake_tasks'
|
4
|
+
SolidusDevSupport::RakeTasks.install
|
5
5
|
|
6
|
-
|
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
|