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