spree_stripe 1.0.2
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 +7 -0
- data/LICENSE.md +14 -0
- data/README.md +132 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_stripe_manifest.js +3 -0
- data/app/controllers/spree/api/v2/storefront/stripe/base_controller.rb +21 -0
- data/app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb +61 -0
- data/app/controllers/spree/api/v2/storefront/stripe/setup_intents_controller.rb +19 -0
- data/app/controllers/spree_stripe/apple_pay_domain_verification_controller.rb +11 -0
- data/app/controllers/spree_stripe/payment_intents_controller.rb +61 -0
- data/app/controllers/spree_stripe/store_controller_decorator.rb +9 -0
- data/app/controllers/stripe_event/webhook_controller_decorator.rb +17 -0
- data/app/helpers/spree_stripe/base_helper.rb +13 -0
- data/app/javascript/spree_stripe/application.js +18 -0
- data/app/javascript/spree_stripe/controllers/stripe_button_controller.js +452 -0
- data/app/javascript/spree_stripe/controllers/stripe_controller.js +311 -0
- data/app/jobs/spree_stripe/base_job.rb +5 -0
- data/app/jobs/spree_stripe/complete_order_job.rb +8 -0
- data/app/jobs/spree_stripe/create_webhook_endpoint_job.rb +7 -0
- data/app/jobs/spree_stripe/register_domain_job.rb +20 -0
- data/app/models/spree_stripe/base.rb +6 -0
- data/app/models/spree_stripe/credit_card_decorator.rb +13 -0
- data/app/models/spree_stripe/custom_domain_decorator.rb +18 -0
- data/app/models/spree_stripe/gateway.rb +366 -0
- data/app/models/spree_stripe/order_decorator.rb +19 -0
- data/app/models/spree_stripe/payment_decorator.rb +46 -0
- data/app/models/spree_stripe/payment_intent.rb +66 -0
- data/app/models/spree_stripe/payment_method_decorator.rb +18 -0
- data/app/models/spree_stripe/payment_methods_webhook_key.rb +8 -0
- data/app/models/spree_stripe/payment_source_decorator.rb +9 -0
- data/app/models/spree_stripe/payment_sources/affirm.rb +9 -0
- data/app/models/spree_stripe/payment_sources/after_pay.rb +13 -0
- data/app/models/spree_stripe/payment_sources/alipay.rb +9 -0
- data/app/models/spree_stripe/payment_sources/ideal.rb +15 -0
- data/app/models/spree_stripe/payment_sources/klarna.rb +9 -0
- data/app/models/spree_stripe/payment_sources/link.rb +13 -0
- data/app/models/spree_stripe/payment_sources/przelewy24.rb +11 -0
- data/app/models/spree_stripe/payment_sources/sepa_debit.rb +13 -0
- data/app/models/spree_stripe/store_decorator.rb +24 -0
- data/app/models/spree_stripe/webhook_key.rb +14 -0
- data/app/presenters/spree_stripe/customer_presenter.rb +35 -0
- data/app/presenters/spree_stripe/payment_intent_presenter.rb +103 -0
- data/app/serializers/spree/api/v2/platform/affirm_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/after_pay_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/alipay_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/ideal_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/klarna_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/link_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/przelewy24_serializer.rb +9 -0
- data/app/serializers/spree/api/v2/platform/sepa_debit_serializer.rb +9 -0
- data/app/serializers/spree/v2/storefront/affirm_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/after_pay_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/alipay_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/ideal_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/klarna_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/link_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/payment_intent_serializer.rb +11 -0
- data/app/serializers/spree/v2/storefront/przelewy24_serializer.rb +7 -0
- data/app/serializers/spree/v2/storefront/sepa_debit_serializer.rb +7 -0
- data/app/services/spree_stripe/complete_order.rb +99 -0
- data/app/services/spree_stripe/create_gateway_webhooks.rb +55 -0
- data/app/services/spree_stripe/create_payment.rb +43 -0
- data/app/services/spree_stripe/create_payment_intent.rb +40 -0
- data/app/services/spree_stripe/create_setup_intent.rb +20 -0
- data/app/services/spree_stripe/create_source.rb +87 -0
- data/app/services/spree_stripe/register_domain.rb +22 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +18 -0
- data/app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb +13 -0
- data/app/services/spree_stripe/webhook_handlers/setup_intent_succeeded.rb +34 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_stripe.html.erb +0 -0
- data/app/views/spree/admin/payment_methods/custom_form_fields/_spree_stripe.html.erb +47 -0
- data/app/views/spree/admin/payment_methods/descriptions/_spree_stripe.html.erb +15 -0
- data/app/views/spree/checkout/payment/_spree_stripe.html.erb +63 -0
- data/app/views/spree_stripe/_head.html.erb +2 -0
- data/app/views/spree_stripe/_quick_checkout.html.erb +34 -0
- data/config/importmap.rb +8 -0
- data/config/initializers/spree.rb +8 -0
- data/config/initializers/stripe.rb +14 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20250310152812_setup_spree_stripe_models.rb +41 -0
- data/lib/generators/spree_stripe/install/install_generator.rb +20 -0
- data/lib/spree_stripe/configuration.rb +5 -0
- data/lib/spree_stripe/engine.rb +36 -0
- data/lib/spree_stripe/factories.rb +3 -0
- data/lib/spree_stripe/testing_support/factories/after_pay_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/alipay_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/gateway_factory.rb +23 -0
- data/lib/spree_stripe/testing_support/factories/ideal_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/klarna_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/link_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/payment_intent_factory.rb +24 -0
- data/lib/spree_stripe/testing_support/factories/payment_source_factory.rb +8 -0
- data/lib/spree_stripe/testing_support/factories/przelewy24_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/sepa_debit_payment_source_factory.rb +6 -0
- data/lib/spree_stripe/testing_support/factories/webhook_key_factory.rb +23 -0
- data/lib/spree_stripe/version.rb +7 -0
- data/lib/spree_stripe.rb +14 -0
- data/vendor/javascript/@stripe--stripe-js--dist--pure.esm.js.js +4 -0
- metadata +295 -0
@@ -0,0 +1,311 @@
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
2
|
+
import showFlashMessage from 'spree/storefront/helpers/show_flash_message'
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static values = {
|
6
|
+
apiKey: String,
|
7
|
+
paymentIntent: Object,
|
8
|
+
returnUrl: String,
|
9
|
+
orderEmail: String,
|
10
|
+
orderToken: String,
|
11
|
+
colorPrimary: String,
|
12
|
+
colorBackground: String,
|
13
|
+
colorText: String
|
14
|
+
}
|
15
|
+
|
16
|
+
static targets = [
|
17
|
+
'paymentElement',
|
18
|
+
'loading',
|
19
|
+
'spinner',
|
20
|
+
'buttonText',
|
21
|
+
'defaultCard'
|
22
|
+
]
|
23
|
+
|
24
|
+
connect() {
|
25
|
+
const stripeOptions = {}
|
26
|
+
|
27
|
+
this.submitTarget = document.querySelector('#checkout-payment-submit')
|
28
|
+
this.billingAddressCheckbox = document.querySelector('#order_use_shipping')
|
29
|
+
this.billingAddressForm = document.querySelector('form.edit_order')
|
30
|
+
this.stripe = Stripe(this.apiKeyValue, stripeOptions)
|
31
|
+
|
32
|
+
if (this.hasDefaultCardTarget) {
|
33
|
+
this.initializeStripe({
|
34
|
+
target: { value: this.defaultCardTarget.value }
|
35
|
+
})
|
36
|
+
} else {
|
37
|
+
this.initializeStripe({ target: { value: 'on' } })
|
38
|
+
}
|
39
|
+
|
40
|
+
// hijack the submit button to call the submit method
|
41
|
+
this.submitTarget.addEventListener('click', this.submit.bind(this))
|
42
|
+
}
|
43
|
+
|
44
|
+
// Fetches a payment intent
|
45
|
+
async initializeStripe(e) {
|
46
|
+
this.setLoading(true)
|
47
|
+
|
48
|
+
if (e.target.value == 'on') {
|
49
|
+
this.stripePaymentMethodId = null
|
50
|
+
} else {
|
51
|
+
this.stripePaymentMethodId = e.target.value
|
52
|
+
}
|
53
|
+
|
54
|
+
const response = await fetch(
|
55
|
+
`/api/v2/storefront/stripe/payment_intents/${this.paymentIntentValue.id}`,
|
56
|
+
{
|
57
|
+
method: 'PATCH',
|
58
|
+
headers: this.spreeApiHeaders,
|
59
|
+
body: JSON.stringify({
|
60
|
+
payment_intent: {
|
61
|
+
stripe_payment_method_id: this.stripePaymentMethodId
|
62
|
+
}
|
63
|
+
})
|
64
|
+
}
|
65
|
+
)
|
66
|
+
const { data, error } = await response.json()
|
67
|
+
|
68
|
+
if (error) {
|
69
|
+
this.handleError(error)
|
70
|
+
return
|
71
|
+
}
|
72
|
+
|
73
|
+
if (this.stripePaymentMethodId === null) {
|
74
|
+
const appearance = {
|
75
|
+
theme: 'stripe',
|
76
|
+
variables: {
|
77
|
+
colorPrimary: this.colorPrimaryValue,
|
78
|
+
colorBackground: this.colorBackgroundValue,
|
79
|
+
colorText: this.colorTextValue
|
80
|
+
}
|
81
|
+
}
|
82
|
+
this.elements = this.stripe.elements({
|
83
|
+
appearance,
|
84
|
+
clientSecret: this.paymentIntentValue.client_secret
|
85
|
+
})
|
86
|
+
const paymentElement = this.elements.create('payment', {
|
87
|
+
fields: {
|
88
|
+
billingDetails: {
|
89
|
+
name: 'never',
|
90
|
+
email: 'never',
|
91
|
+
address: {
|
92
|
+
country: 'never',
|
93
|
+
postalCode: 'never'
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
})
|
98
|
+
paymentElement.mount(this.paymentElementTarget)
|
99
|
+
paymentElement.on('change', (event) => {
|
100
|
+
this.selectedMethod = event.value?.type
|
101
|
+
})
|
102
|
+
}
|
103
|
+
|
104
|
+
this.setLoading(false)
|
105
|
+
}
|
106
|
+
|
107
|
+
async submit(e) {
|
108
|
+
e.preventDefault()
|
109
|
+
this.setLoading(true)
|
110
|
+
|
111
|
+
const validated = await this.validateOrderForPayment()
|
112
|
+
|
113
|
+
if (!validated) {
|
114
|
+
this.setLoading(false)
|
115
|
+
e.stopImmediatePropagation()
|
116
|
+
return
|
117
|
+
}
|
118
|
+
|
119
|
+
const billingAddress = await this.updateBillingAddress()
|
120
|
+
|
121
|
+
if (!billingAddress) {
|
122
|
+
this.setLoading(false)
|
123
|
+
e.stopImmediatePropagation()
|
124
|
+
return
|
125
|
+
}
|
126
|
+
|
127
|
+
if (this.stripePaymentMethodId) {
|
128
|
+
const { paymentIntent, error } = await this.stripe.confirmCardPayment(
|
129
|
+
this.paymentIntentValue.client_secret
|
130
|
+
)
|
131
|
+
|
132
|
+
this.handleError(error)
|
133
|
+
|
134
|
+
window.location.href = this.returnUrlValue
|
135
|
+
} else {
|
136
|
+
const elements = this.elements
|
137
|
+
|
138
|
+
if (!elements || !elements.getElement('payment')) {
|
139
|
+
showFlashMessage(
|
140
|
+
'An unexpected error occurred. Please refresh the page and try again.',
|
141
|
+
'error'
|
142
|
+
)
|
143
|
+
return
|
144
|
+
}
|
145
|
+
|
146
|
+
const { error } = await this.stripe.confirmPayment({
|
147
|
+
elements,
|
148
|
+
confirmParams: {
|
149
|
+
payment_method_data: {
|
150
|
+
billing_details: {
|
151
|
+
email: this.orderEmailValue,
|
152
|
+
name: billingAddress.firstname + ' ' + billingAddress.lastname,
|
153
|
+
address: {
|
154
|
+
city: billingAddress.city,
|
155
|
+
country: billingAddress.country_iso,
|
156
|
+
line1: billingAddress.address1,
|
157
|
+
line2: billingAddress.address2,
|
158
|
+
postal_code: billingAddress.zipcode,
|
159
|
+
state: billingAddress.state_code
|
160
|
+
}
|
161
|
+
}
|
162
|
+
},
|
163
|
+
return_url: this.returnUrlValue
|
164
|
+
}
|
165
|
+
})
|
166
|
+
|
167
|
+
// This point will only be reached if there is an immediate error when
|
168
|
+
// confirming the payment. Otherwise, your customer will be redirected to
|
169
|
+
// your `return_url`. For some payment methods like iDEAL, your customer will
|
170
|
+
// be redirected to an intermediate site first to authorize the payment, then
|
171
|
+
// redirected to the `return_url`.
|
172
|
+
// TODO: we need to remove the payment intent id from this order and create new
|
173
|
+
this.handleError(error)
|
174
|
+
}
|
175
|
+
|
176
|
+
this.setLoading(false)
|
177
|
+
}
|
178
|
+
|
179
|
+
async validateOrderForPayment() {
|
180
|
+
const response = await fetch(
|
181
|
+
'/api/v2/storefront/checkout/validate_order_for_payment',
|
182
|
+
{
|
183
|
+
method: 'POST',
|
184
|
+
headers: this.spreeApiHeaders
|
185
|
+
}
|
186
|
+
)
|
187
|
+
|
188
|
+
const json = await response.json()
|
189
|
+
|
190
|
+
if (json.meta?.messages?.length) {
|
191
|
+
const message = [
|
192
|
+
json.meta.messages,
|
193
|
+
'please refresh the page and try again.'
|
194
|
+
].join(', ')
|
195
|
+
|
196
|
+
showFlashMessage(message, 'error')
|
197
|
+
return false
|
198
|
+
} else {
|
199
|
+
return true
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
async updateBillingAddress() {
|
204
|
+
// billing address same as shipping address
|
205
|
+
if (this.billingAddressCheckbox.checked) {
|
206
|
+
const response = await fetch('/api/v2/storefront/checkout?include=billing_address', {
|
207
|
+
method: 'PATCH',
|
208
|
+
headers: this.spreeApiHeaders,
|
209
|
+
body: JSON.stringify({
|
210
|
+
order: {
|
211
|
+
use_shipping: true
|
212
|
+
}
|
213
|
+
})
|
214
|
+
});
|
215
|
+
|
216
|
+
const responseJson = await response.json();
|
217
|
+
|
218
|
+
if (response.ok) {
|
219
|
+
return responseJson.included.find(item => item.type === 'address').attributes;
|
220
|
+
} else {
|
221
|
+
this.submitTarget.disabled = false;
|
222
|
+
return false;
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
// billing address different from shipping address
|
227
|
+
if (this.billingAddressForm.checkValidity()) {
|
228
|
+
const formData = new FormData(this.billingAddressForm);
|
229
|
+
|
230
|
+
const response = await fetch('/api/v2/storefront/checkout?include=billing_address', {
|
231
|
+
method: 'PATCH',
|
232
|
+
headers: this.spreeApiHeaders,
|
233
|
+
body: JSON.stringify({
|
234
|
+
order: {
|
235
|
+
bill_address_attributes: {
|
236
|
+
firstname: formData.get("order[bill_address_attributes][firstname]"),
|
237
|
+
lastname: formData.get("order[bill_address_attributes][lastname]"),
|
238
|
+
address1: formData.get("order[bill_address_attributes][address1]"),
|
239
|
+
address2: formData.get("order[bill_address_attributes][address2]"),
|
240
|
+
city: formData.get("order[bill_address_attributes][city]"),
|
241
|
+
country_id: formData.get("order[bill_address_attributes][country_id]"),
|
242
|
+
state_id: formData.get("order[bill_address_attributes][state_id]"),
|
243
|
+
state_name: formData.get("order[bill_address_attributes][state_name]"),
|
244
|
+
zipcode: formData.get("order[bill_address_attributes][zipcode]"),
|
245
|
+
phone: formData.get("order[bill_address_attributes][phone]")
|
246
|
+
}
|
247
|
+
}
|
248
|
+
})
|
249
|
+
});
|
250
|
+
|
251
|
+
const responseJson = await response.json();
|
252
|
+
|
253
|
+
if (response.ok) {
|
254
|
+
return responseJson.included.find(item => item.type === 'address').attributes;
|
255
|
+
} else {
|
256
|
+
const errors = Array.isArray(responseJson.error) ? responseJson.error.join('. ') : responseJson.error || 'Billing address is invalid';
|
257
|
+
showFlashMessage(errors, 'error');
|
258
|
+
|
259
|
+
const flashMessage = document.getElementsByClassName('flash-message')[0];
|
260
|
+
if (flashMessage) {
|
261
|
+
flashMessage.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
|
262
|
+
}
|
263
|
+
|
264
|
+
this.submitTarget.disabled = false;
|
265
|
+
return false;
|
266
|
+
}
|
267
|
+
} else {
|
268
|
+
this.submitTarget.disabled = false;
|
269
|
+
return false
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
handleError(error) {
|
274
|
+
if (error) {
|
275
|
+
if (error.type === 'card_error' || error.type === 'validation_error') {
|
276
|
+
showFlashMessage(error.message, 'error')
|
277
|
+
} else {
|
278
|
+
showFlashMessage(
|
279
|
+
'An unexpected error occured. Please refresh the page and try again.',
|
280
|
+
'error'
|
281
|
+
)
|
282
|
+
}
|
283
|
+
|
284
|
+
this.submitTarget.disabled = false;
|
285
|
+
|
286
|
+
window.scrollTo({
|
287
|
+
top: 0,
|
288
|
+
behavior: 'smooth'
|
289
|
+
})
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
// Show a spinner on payment submission
|
294
|
+
setLoading(isLoading) {
|
295
|
+
if (isLoading) {
|
296
|
+
// Disable the button and show a spinner
|
297
|
+
this.submitTarget.disabled = true
|
298
|
+
} else {
|
299
|
+
this.loadingTarget.classList.add('hidden')
|
300
|
+
this.submitTarget.disabled = false
|
301
|
+
this.submitTarget.classList.remove('hidden')
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
get spreeApiHeaders() {
|
306
|
+
return {
|
307
|
+
'X-Spree-Order-Token': this.orderTokenValue,
|
308
|
+
'Content-Type': 'application/json'
|
309
|
+
}
|
310
|
+
}
|
311
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SpreeStripe
|
2
|
+
class RegisterDomainJob < BaseJob
|
3
|
+
def perform(model_id, klass_type = 'store')
|
4
|
+
@klass_type = klass_type
|
5
|
+
model = klass.find(model_id)
|
6
|
+
RegisterDomain.new.call(model: model)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def klass
|
12
|
+
@klass ||= case @klass_type
|
13
|
+
when 'store'
|
14
|
+
Spree::Store
|
15
|
+
when 'custom_domain'
|
16
|
+
Spree::CustomDomain
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module SpreeStripe
|
2
|
+
module CreditCardDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.store_accessor :private_metadata, :wallet
|
5
|
+
end
|
6
|
+
|
7
|
+
def wallet_type
|
8
|
+
wallet&.[]('type')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Spree::CreditCard.prepend(SpreeStripe::CreditCardDecorator)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SpreeStripe
|
2
|
+
module CustomDomainDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.after_create :register_stripe_domain
|
5
|
+
|
6
|
+
base.store_accessor :private_metadata, :stripe_apple_pay_domain_id
|
7
|
+
base.store_accessor :private_metadata, :stripe_top_level_domain_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def register_stripe_domain
|
11
|
+
return if store.stripe_gateway.blank?
|
12
|
+
|
13
|
+
SpreeStripe::RegisterDomainJob.perform_later(id, 'custom_domain')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Spree::CustomDomain.prepend(SpreeStripe::CustomDomainDecorator)
|