PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.7
Booking for Appointments and Events Calendar – Amelia v2.4.7
2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / views / public / Parts / Payment / Methods / PaymentStripe.vue
ameliabooking / v3 / src / views / public / Parts / Payment / Methods Last commit date
PaymentCommon.vue 1 month ago PaymentOnSite.vue 1 month ago PaymentPayPal.vue 1 month ago PaymentSquare.vue 1 month ago PaymentStripe.vue 1 month ago PaymentWc.vue 1 month ago
PaymentStripe.vue
659 lines
1 <template>
2 <div class="am-fs__payment-stripe" :style="cssVars">
3 <div v-if="supportsExpressCheckout" class="am-fs__payment-stripe__express-checkout">
4 <div :id="'am-stripe-prb-' + shortcodeData.counter" class="am-stripe-prb"></div>
5 </div>
6
7 <div v-if="paymentRequestAvailable" class="am-fs__payment-divider">
8 <span class="am-divider-text">{{ amLabels.payment_or_pay_with_card }}</span>
9 </div>
10
11 <div v-if="amSettings.payments.stripe.address" class="am-fs__payment-stripe__card">
12 <div :id="'am-stripe-address-' + shortcodeData.counter" class="am-stripe-address"></div>
13 </div>
14 <div class="am-fs__payment-stripe__card">
15 <div>
16 <p>{{ amLabels.card_number_colon }}:</p>
17 <div :id="'am-stripe-cn-' + shortcodeData.counter" class="am-stripe-cn"></div>
18 </div>
19 <div>
20 <div>
21 <p>{{ amLabels.expires_date_colon }}:</p>
22 <div :id="'am-stripe-ed-' + shortcodeData.counter" class="am-stripe-ed"></div>
23 </div>
24 <div>
25 <p>CVC:</p>
26 <div :id="'am-stripe-cvc-' + shortcodeData.counter" class="am-stripe-cvc"></div>
27 </div>
28 </div>
29 </div>
30 <div class="am-fs__payment-stripe__policy">
31 <p>
32 {{ amLabels.payment_protected_policy }}
33 </p>
34 <img
35 :src="baseUrls.wpAmeliaPluginURL + '/v3/src/assets/img/icons/stripe.svg'"
36 alt="Stripe policy"
37 />
38 <span>
39 {{ amLabels.stripe }}
40 </span>
41 </div>
42
43 <div
44 v-if="amSettings.general.googleRecaptcha.enabled"
45 :id="'recaptcha-' + shortcodeData.counter"
46 class="am-recaptcha-holder"
47 >
48 <vue-recaptcha
49 ref="recaptchaRef"
50 :size="amSettings.general.googleRecaptcha.invisible ? 'invisible' : null"
51 :load-recaptcha-script="true"
52 :sitekey="amSettings.general.googleRecaptcha.siteKey"
53 @verify="onRecaptchaVerify"
54 @expired="onRecaptchaExpired"
55 >
56 </vue-recaptcha>
57 </div>
58 </div>
59 </template>
60
61 <script setup>
62 import { computed, onMounted, inject, watchEffect, ref, watch, nextTick } from 'vue'
63 import {
64 usePaymentError,
65 useBookingData,
66 useCreateBooking,
67 useCreateBookingSuccess,
68 useCreateBookingError,
69 getErrorMessage,
70 } from '../../../../../assets/js/public/booking.js'
71 import { useColorTransparency } from '../../../../../assets/js/common/colorManipulation.js'
72
73 // * Import from Vuex
74 import { useStore } from 'vuex'
75 import { useScrollTo } from '../../../../../assets/js/common/scrollElements'
76 import { VueRecaptcha } from 'vue-recaptcha'
77 import httpClient from '../../../../../plugins/axios'
78
79 const store = useStore()
80
81 // * Global settings
82 const amSettings = inject('settings')
83
84 // * local language short code
85 const localLanguage = inject('localLanguage')
86
87 // * Labels
88 const amLabels = inject('amLabels')
89
90 // * Base Urls
91 const baseUrls = inject('baseUrls')
92
93 const shortcodeData = inject('shortcodeData')
94
95 // * Step Functions
96 const { nextStep, footerButtonReset, footerButtonClicked } = inject('changingStepsFunctions', {
97 nextStep: () => {},
98 footerButtonReset: () => {},
99 footerButtonClicked: {
100 value: false,
101 },
102 })
103
104 // * Components Emits
105 const emits = defineEmits(['payment-error'])
106
107 /*************
108 * Recaptcha *
109 ************/
110
111 let recaptchaRef = ref(null)
112
113 let recaptchaValid = ref(false)
114
115 let recaptchaResponse = ref(null)
116
117 // * Express Checkout
118 let paymentRequest = null
119 let paymentRequestButton = null
120 let supportsExpressCheckout = ref(false)
121 let paymentRequestAvailable = ref(false)
122
123 async function initializeExpressCheckout(update = false) {
124 let checkoutPaymentData = null
125
126 await httpClient
127 .post('/payments/amount', useBookingData(store, null, true, {}, null)['data'])
128 .then((response) => {
129 checkoutPaymentData = response.data.data
130 })
131 .catch((e) => {
132 const message = e?.response?.data?.message || e.message || 'Unknown error'
133 emits('payment-error', message)
134 })
135
136 if (!update) {
137 stripePaymentInit(
138 checkoutPaymentData?.transfers?.accounts &&
139 Object.keys(checkoutPaymentData.transfers.accounts).length === 1 &&
140 checkoutPaymentData?.transfers?.method === 'direct'
141 ? Object.keys(checkoutPaymentData.transfers.accounts)[0]
142 : null,
143 )
144 }
145
146 if (!stripeObject) return
147
148 supportsExpressCheckout.value = true
149
150 const totalPriceParts = new Intl.NumberFormat('en-US', {
151 style: 'currency',
152 currency: checkoutPaymentData.currency,
153 }).formatToParts(checkoutPaymentData.amount)
154
155 const IntegerPart = totalPriceParts
156 .filter((part) => part.type === 'integer')
157 .map((part) => part.value)
158 .join('')
159 const fractionPart = totalPriceParts.find((part) => part.type === 'fraction')?.value || ''
160 const totalPriceInMinorUnits = Number.parseInt(`${IntegerPart}${fractionPart}`)
161
162 paymentRequest = stripeObject.paymentRequest({
163 country: 'US',
164 currency: checkoutPaymentData.currency.toLowerCase(),
165 total: {
166 label: 'total',
167 amount: totalPriceInMinorUnits,
168 },
169 requestPayerName: true,
170 requestPayerEmail: true,
171 })
172
173 paymentRequest
174 .canMakePayment()
175 .then(function (result) {
176 if (result) {
177 paymentRequestAvailable.value = true
178
179 paymentRequestButton = stripeObject.elements().create('paymentRequestButton', {
180 paymentRequest: paymentRequest,
181 style: {
182 paymentRequestButton: {
183 type: 'default',
184 theme: 'dark',
185 height: '40px',
186 },
187 },
188 })
189
190 paymentRequestButton.mount('#am-stripe-prb-' + shortcodeData.value.counter)
191
192 paymentRequest.on('paymentmethod', handleExpressCheckout)
193 } else {
194 paymentRequestAvailable.value = false
195 }
196 })
197 .catch((error) => {
198 console.error('Error checking payment capabilities:', error)
199 paymentRequestAvailable.value = false
200 })
201 }
202
203 async function handleExpressCheckout(event) {
204 try {
205 const { paymentMethod } = event
206 const addressResult = amSettings.payments.stripe.address ? await address.getValue() : null
207
208 useCreateBooking(
209 store,
210 useBookingData(
211 store,
212 null,
213 false,
214 {
215 paymentMethodId: paymentMethod.id,
216 address: addressResult ? addressResult.value : null,
217 },
218 recaptchaResponse.value,
219 ),
220 function (response) {
221 if (response.data.data.requiresAction) {
222 stripePaymentActionRequired({
223 ...response.data.data,
224 expressCheckoutEvent: event,
225 })
226 return
227 }
228 event.complete('success')
229 successBooking(response)
230 },
231 (response) => {
232 event.complete('fail')
233 errorBooking(response)
234 },
235 )
236 } catch (error) {
237 event.complete('fail')
238 emits('payment-error', error.message)
239 }
240 }
241
242 function onRecaptchaExpired() {
243 recaptchaValid.value = false
244
245 emits('payment-error', amLabels.recaptcha_error)
246 }
247
248 function onRecaptchaVerify(response) {
249 recaptchaValid.value = true
250
251 recaptchaResponse.value = response
252
253 if (amSettings.general.googleRecaptcha.invisible) {
254 stripePaymentCreate(useBookingData(store, null, false, {}, recaptchaResponse.value))
255
256 return false
257 }
258 }
259
260 // * Payment Part
261 let stripeObject = null
262 // let stripeCard = null
263 let cardNum = null
264 let cardEd = null
265 let cardCvc = null
266
267 let address = null
268
269 // * Colors
270 let amColors = inject('amColors')
271 let amFonts = inject('amFonts')
272
273 function stripePaymentInit(accountId) {
274 const options = {
275 locale: localLanguage.value.replace('_', '-'),
276 }
277
278 if (accountId) {
279 options.stripeAccount = accountId
280 }
281
282 stripeObject = Stripe(
283 amSettings.payments.stripe.testMode === false
284 ? amSettings.payments.stripe.livePublishableKey
285 : amSettings.payments.stripe.testPublishableKey,
286 options,
287 )
288
289 let elements = stripeObject.elements()
290
291 // stripeCard = elements.create('card')
292 // TODO - set unique ID with counter
293 // stripeCard.mount('#am-stripe-element')
294
295 let style = {
296 base: {
297 color: amColors.value.colorInpText,
298 fontSize: '15px',
299 fontFamily: amFonts.value.fontFamily,
300 },
301 }
302
303 cardNum = elements.create('cardNumber', { style })
304 cardNum.mount('#am-stripe-cn-' + shortcodeData.value.counter)
305 cardEd = elements.create('cardExpiry', { style })
306 cardEd.mount('#am-stripe-ed-' + shortcodeData.value.counter)
307 cardCvc = elements.create('cardCvc', { style })
308 cardCvc.mount('#am-stripe-cvc-' + shortcodeData.value.counter)
309
310 if (amSettings.payments.stripe.address) {
311 address = elements.create('address', { mode: 'billing' })
312 address.mount('#am-stripe-address-' + shortcodeData.value.counter)
313 }
314 }
315
316 async function stripePaymentCreate() {
317 if (
318 amSettings.general.googleRecaptcha.enabled &&
319 !amSettings.general.googleRecaptcha.invisible &&
320 !recaptchaValid.value
321 ) {
322 emits('payment-error', amLabels.recaptcha_error)
323
324 return false
325 }
326
327 let addressResult = null
328 if (amSettings.payments.stripe.address && address) {
329 addressResult = await address.getValue()
330 }
331
332 stripeObject
333 .createPaymentMethod('card', cardNum, {
334 billing_details: {
335 ...(addressResult ? addressResult.value : {}),
336 },
337 })
338 .then(function (result) {
339 if (stripeError(result, addressResult)) {
340 store.commit('setLoading', false)
341 return
342 }
343
344 useCreateBooking(
345 store,
346 useBookingData(
347 store,
348 null,
349 false,
350 {
351 paymentMethodId: result.paymentMethod.id,
352 address: addressResult ? addressResult.value : null,
353 },
354 recaptchaResponse.value,
355 ),
356 function (response) {
357 if (response.data.data.requiresAction) {
358 stripePaymentActionRequired(response.data.data)
359
360 return
361 }
362
363 store.commit('setLoading', false)
364 successBooking(response)
365 },
366 (response) => {
367 store.commit('setLoading', false)
368 errorBooking(response)
369 },
370 )
371 })
372 }
373
374 function stripePaymentActionRequired(response) {
375 const { expressCheckoutEvent } = response || {}
376
377 stripeObject
378 .handleNextAction({
379 clientSecret: response.paymentIntentClientSecret,
380 })
381 .then(async function (result) {
382 let addressResult = null
383 if (amSettings.payments.stripe.address && address && !expressCheckoutEvent) {
384 addressResult = await address.getValue()
385 }
386
387 if (stripeError(result, addressResult)) {
388 if (expressCheckoutEvent) {
389 expressCheckoutEvent.complete('fail')
390 }
391 return
392 }
393
394 useCreateBooking(
395 store,
396 useBookingData(
397 store,
398 null,
399 false,
400 {
401 paymentIntentId: result.paymentIntent.id,
402 address: addressResult ? addressResult.value : null,
403 },
404 null,
405 ),
406 function (response) {
407 if (expressCheckoutEvent) {
408 expressCheckoutEvent.complete('success')
409 }
410 successBooking(response)
411 },
412 function (error) {
413 if (expressCheckoutEvent) {
414 expressCheckoutEvent.complete('fail')
415 }
416 errorBooking(error)
417 },
418 )
419 })
420 .catch((error) => {
421 if (expressCheckoutEvent) {
422 expressCheckoutEvent.complete('fail')
423 }
424 emits('payment-error', error.message || 'Authentication failed')
425 })
426 }
427
428 function stripeError(result, addressResult) {
429 let addressError = addressResult && !addressResult.complete
430 if (result.error || addressError) {
431 usePaymentError(store, function () {
432 emits('payment-error', addressError ? amLabels.payment_address_error : result.error.message)
433 })
434 return true
435 }
436
437 return false
438 }
439
440 function successBooking(response) {
441 useCreateBookingSuccess(store, response, function () {
442 nextStep()
443 })
444 }
445
446 function errorBooking(error) {
447 useCreateBookingError(store, error.response.data, () => {
448 emits('payment-error', getErrorMessage())
449 })
450 }
451
452 let paymentStepRef = inject('paymentRef')
453
454 function continueWithBooking() {
455 footerButtonReset()
456
457 store.commit('setLoading', true)
458
459 if (amSettings.general.googleRecaptcha.enabled) {
460 if (amSettings.general.googleRecaptcha.invisible) {
461 recaptchaRef.value.execute()
462 } else if (!recaptchaValid.value) {
463 emits('payment-error', amLabels.value.recaptcha_error)
464 store.commit('setLoading', false)
465 } else {
466 stripePaymentCreate()
467 }
468 } else {
469 stripePaymentCreate()
470 }
471 }
472
473 // * Watching when footer button was clicked
474 watchEffect(
475 () => {
476 if (footerButtonClicked.value) {
477 if (!store.getters['coupon/getCouponValidated']) {
478 footerButtonReset()
479 useScrollTo(paymentStepRef.value, paymentStepRef.value, 20, 300)
480 emits('payment-error', amLabels.value.coupon_mandatory)
481 } else {
482 continueWithBooking()
483 }
484 }
485 },
486 { flush: 'post' },
487 )
488
489 // * Watch coupon and payment deposit changes and update paymentRequest amount
490 watch(
491 [() => store.getters['coupon/getCoupon'], () => store.getters['payment/getPaymentDeposit']],
492 async (newValues, oldValues) => {
493 const [newCoupon, newDeposit] = newValues || []
494 const [oldCoupon, oldDeposit] = oldValues || []
495
496 if (
497 paymentRequest &&
498 (newCoupon.deduction || newCoupon.discount || newDeposit !== oldDeposit)
499 ) {
500 if (paymentRequestButton) {
501 paymentRequestButton.unmount()
502 paymentRequestButton = null
503 }
504 // Wait for next tick to ensure DOM is updated
505 await nextTick()
506 initializeExpressCheckout(true)
507 }
508 },
509 )
510
511 onMounted(() => {
512 initializeExpressCheckout(false)
513 })
514
515 // * Css variables
516 let cssVars = computed(() => {
517 return {
518 '--am-c-pay-text': amColors.value.colorMainText,
519 '--am-c-pay-text-op60': useColorTransparency(amColors.value.colorMainText, 0.6),
520 }
521 })
522 </script>
523
524 <script>
525 export default {
526 name: 'PaymentStripe',
527 }
528 </script>
529
530 <style lang="scss">
531 .amelia-v2-booking {
532 #amelia-container {
533 $pay: am-fs__payment-stripe;
534 .#{$pay} {
535 .#{$pay}__policy {
536 display: flex;
537 align-items: center;
538 justify-content: center;
539 color: var(--am-c-pay-text-op60);
540 margin: 20px 0 10px;
541
542 p {
543 display: inline-flex;
544 font-size: 14px;
545 font-weight: 500;
546 line-height: 1.42857;
547 color: var(--am-c-ps-text);
548 }
549
550 img {
551 margin: 0 8px;
552 }
553
554 span {
555 display: inline-flex;
556 font-size: 14px;
557 font-weight: 500;
558 line-height: 1.42857;
559 color: #6772e5;
560 }
561 }
562
563 &__card,
564 &__address {
565 p {
566 font-size: 15px;
567 font-weight: 500;
568 line-height: 1.33333;
569 color: var(--am-c-pay-text);
570 margin-bottom: 4px;
571 }
572
573 & > div:nth-child(2) {
574 display: flex;
575 gap: 16px;
576
577 & > div:first-child {
578 width: 60%;
579 }
580
581 & > div:nth-child(2) {
582 width: 40%;
583 }
584 }
585
586 .am-stripe-cn,
587 .am-stripe-ed,
588 .am-stripe-cvc {
589 background-color: var(--am-c-inp-bgr);
590 border: 1px solid var(--am-c-inp-border);
591 border-radius: 6px;
592 box-shadow: 0 2px 2px rgb(14 25 32 / 3%);
593 box-sizing: border-box;
594 padding: 12px;
595 }
596 .am-stripe-cn {
597 margin-bottom: 16px;
598 }
599
600 .am-stripe-address {
601 & > div {
602 width: 100% !important;
603 }
604 margin-bottom: 16px;
605 }
606 }
607
608 .am-recaptcha-holder {
609 height: 84px;
610 position: relative;
611
612 & > div > div {
613 position: absolute !important;
614 }
615 }
616
617 .am-fs__payment-stripe__express-checkout {
618 margin-bottom: 20px;
619
620 .am-stripe-prb {
621 margin-bottom: 10px;
622 }
623
624 p {
625 color: var(--am-c-pay-text-op60);
626 font-size: 14px;
627 }
628 }
629
630 .am-fs__payment-divider {
631 display: flex;
632 align-items: center;
633 justify-content: center;
634 width: 100%;
635 margin: 16px 0;
636 text-align: center;
637 }
638
639 .am-fs__payment-divider::before,
640 .am-fs__payment-divider::after {
641 content: '';
642 flex-grow: 1;
643 height: 1px;
644 background-color: var(--am-c-pay-text-op60);
645 margin: 0 8px;
646 }
647
648 .am-divider-text {
649 font-size: 14px;
650 color: var(--am-c-pay-text-op60);
651 text-transform: uppercase;
652 line-height: 1.33333;
653 font-weight: 500;
654 }
655 }
656 }
657 }
658 </style>
659