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 / PaymentSquare.vue
ameliabooking / v3 / src / views / public / Parts / Payment Last commit date
Coupon 1 month ago Info 1 month ago Methods 1 month ago Page 1 month ago Coupon.vue 1 month ago 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
PaymentSquare.vue
490 lines
1 <template>
2 <div>
3 <div v-show="squareLoading" class="am-fs__square-loading" :style="cssVars">
4 <!-- Skeleton -->
5 <el-skeleton animated>
6 <el-skeleton-item />
7 </el-skeleton>
8 <!-- /Skeleton -->
9 </div>
10 <!-- Credit Card via Square-->
11 <div v-show="!squareLoading" class="am-fs__payment-square" :style="cssVars">
12 <div class="am-fs__payment-square__google-pay">
13 <div id="google-pay-button" />
14 </div>
15
16 <div v-if="applePayReady" class="am-fs__payment-square__apple-pay">
17 <div id="apple-pay-button" />
18 </div>
19
20 <div class="am-fs__payment-divider">
21 <span class="am-divider-text">{{ amLabels.payment_or_pay_with_card }}</span>
22 </div>
23
24 <div id="payment-status-container"></div>
25 <div id="card-container"></div>
26 </div>
27 <!-- /Credit Card via Square-->
28 </div>
29 </template>
30
31 <script setup>
32 import { computed, inject, onMounted, ref, watchEffect, nextTick, watch } from 'vue'
33 import {
34 getErrorMessage,
35 useBookingData,
36 useCreateBooking,
37 useCreateBookingError,
38 useCreateBookingSuccess,
39 } from '../../../../assets/js/public/booking.js'
40 import { useColorTransparency } from '../../../../assets/js/common/colorManipulation.js'
41 import { useStore } from 'vuex'
42 import httpClient from '../../../../plugins/axios'
43 import { useScrollTo } from '../../../../assets/js/common/scrollElements'
44
45 // * Global settings
46 const amSettings = inject('settings')
47 const store = useStore()
48
49 // * Labels
50 const amLabels = inject('amLabels')
51
52 // * Colors
53 let amColors = inject('amColors')
54
55 // * Css variables
56 let cssVars = computed(() => {
57 return {
58 '--am-c-pay-text': amColors.value.colorMainText,
59 '--am-c-pay-text-op60': useColorTransparency(amColors.value.colorMainText, 0.6),
60 }
61 })
62
63 // * Components Emits
64 const emits = defineEmits(['payment-error'])
65
66 const { nextStep, footerButtonReset, footerButtonClicked } = inject('changingStepsFunctions', {
67 nextStep: () => {},
68 footerButtonReset: () => {},
69 footerButtonClicked: {
70 value: false,
71 },
72 })
73
74 const cardInstance = ref(null)
75
76 async function continueWithBooking() {
77 footerButtonReset()
78 store.commit('booking/setLoading', true)
79
80 if (!cardInstance.value) {
81 store.commit('booking/setLoading', true)
82 }
83
84 const totalAmount = await payingNow()
85 const token = await squareTokenize(cardInstance.value, totalAmount.formattedAmount)
86 if (!token) {
87 store.commit('booking/setLoading', false)
88 return
89 }
90 await createSquarePayment(token)
91 }
92
93 // * Watching when footer button was clicked
94 watchEffect(() => {
95 if (footerButtonClicked.value) {
96 if (!store.getters['booking/getCouponValidated']) {
97 footerButtonReset()
98 emits('payment-error', amLabels.value.coupon_mandatory)
99 } else {
100 continueWithBooking()
101 }
102 }
103 })
104
105 const cardReady = ref(false)
106 const googlePayReady = ref(false)
107 const applePayReady = ref(true)
108 const squareLoading = computed(() => !cardReady.value)
109 const paymentTotalAmount = ref(null)
110
111 let paymentRequest = null
112 const walletInstance = ref({ googlePay: null, applePay: null })
113
114 // * Watch coupon changes and payment deposit changes to update paymentRequest amount
115 // Apple Pay - No async operations can be called between the user gesture (click) and tokenize().
116 watch(
117 [() => store.getters['booking/getCoupon'], () => store.getters['booking/getPaymentDeposit']],
118 async () => {
119 if (paymentRequest && store.getters['booking/getCouponValidated']) {
120 // Wait for next tick to ensure DOM is updated
121 await nextTick()
122 paymentRequest = await buildPaymentRequest()
123
124 const initWallet = async (method, target, onError) => {
125 try {
126 walletInstance.value[target] = await payments[method](paymentRequest)
127 } catch (err) {
128 console.log(err)
129 if (onError) onError()
130 }
131 }
132
133 await initWallet('googlePay', 'googlePay')
134 await initWallet('applePay', 'applePay', () => {
135 applePayReady.value = false
136 })
137 }
138 },
139 )
140
141 async function getAmountToPay() {
142 let checkoutPaymentData = null
143
144 await httpClient
145 .post('/payments/amount', useBookingData(store, null, true, {}, null)['data'])
146 .then((response) => {
147 checkoutPaymentData = response.data.data
148 })
149 .catch((e) => {
150 const message = e?.response?.data?.message || e.message || 'Unknown error'
151 emits('payment-error', message)
152 })
153
154 const totalPriceParts = new Intl.NumberFormat('en-US', {
155 style: 'currency',
156 currency: checkoutPaymentData.currency,
157 }).formatToParts(checkoutPaymentData.amount)
158
159 const IntegerPart = totalPriceParts.find((part) => part.type === 'integer')?.value || ''
160 const fractionPart = totalPriceParts.find((part) => part.type === 'fraction')?.value || ''
161 const decimalPart = totalPriceParts.find((part) => part.type === 'decimal')?.value || ''
162 const formattedAmount = `${IntegerPart}${decimalPart}${fractionPart}`
163 paymentTotalAmount.value = formattedAmount
164
165 return {
166 formattedAmount,
167 rawAmount: checkoutPaymentData.amount,
168 countryCode: checkoutPaymentData.countryCode,
169 }
170 }
171
172 async function payingNow() {
173 return await getAmountToPay()
174 }
175
176 onMounted(async () => {
177 // Defer mounting logic until DOM is visible
178 cardReady.value = false
179 googlePayReady.value = false
180
181 await nextTick()
182 await initSquarePayment()
183 })
184
185 const squareLocationId = amSettings.payments.square.locationId
186 const squareClientId = amSettings.payments.square.testMode
187 ? amSettings.payments.square.clientTestId
188 : amSettings.payments.square.clientLiveId
189
190 const bookingData = useBookingData(store, null, true, {}, null)
191
192 let paymentStepRef = inject('paymentRef')
193
194 const payments = window.Square.payments(squareClientId, squareLocationId)
195
196 async function buildPaymentRequest() {
197 try {
198 const paymentInfo = await payingNow()
199 if (!paymentInfo) return null
200 return payments.paymentRequest({
201 countryCode: paymentInfo.countryCode,
202 currencyCode: bookingData.data.payment.currency,
203 total: {
204 amount: paymentInfo.formattedAmount.toString(),
205 label: 'Total',
206 },
207 })
208 } catch (e) {
209 console.log(e)
210 return null
211 }
212 }
213
214 async function setupDigitalWallet({ buttonId, type, readyRef }) {
215 try {
216 const btnEl = document.getElementById(buttonId)
217 if (!btnEl) return
218
219 if (type === 'googlePay') {
220 btnEl.innerHTML = ''
221 }
222
223 if (!paymentRequest) return
224
225 walletInstance.value[type] = await payments[type](paymentRequest)
226
227 // Google Pay needs to render its own button via attach
228 if (type === 'googlePay') {
229 await walletInstance.value[type].attach(`#${buttonId}`)
230 }
231
232 readyRef.value = true
233
234 btnEl.addEventListener('click', async () => {
235 store.commit('setLoading', true)
236 store.commit('booking/setLoading', true)
237
238 const token = await squareTokenize(walletInstance.value[type], paymentTotalAmount.value)
239 if (!token) {
240 store.commit('setLoading', false)
241 store.commit('booking/setLoading', false)
242 return
243 }
244 await createSquarePayment(token)
245 })
246 } catch (e) {
247 console.log(e)
248 if (type === 'applePay') {
249 readyRef.value = false
250 }
251 }
252 }
253
254 async function initSquarePayment() {
255 const squareCardStyle = {
256 '.input-container': {
257 borderColor: '#d9d9d9',
258 borderRadius: '6px',
259 },
260 }
261
262 try {
263 const cardContainer = document.getElementById('card-container')
264 if (cardContainer) {
265 cardContainer.innerHTML = ''
266 }
267 const card = await payments.card({
268 style: squareCardStyle,
269 })
270 await card.attach('#card-container')
271 cardInstance.value = card
272
273 cardReady.value = true
274 } catch (e) {
275 const statusContainer = document.getElementById('payment-status-container')
276 console.error(e)
277 store.commit('setLoading', false)
278 store.commit('booking/setLoading', false)
279
280 if (statusContainer) {
281 statusContainer.className = 'missing-credentials'
282 statusContainer.style.visibility = 'visible'
283 }
284 }
285
286 paymentRequest = await buildPaymentRequest()
287 await setupDigitalWallet({
288 buttonId: 'google-pay-button',
289 type: 'googlePay',
290 readyRef: googlePayReady,
291 })
292 await setupDigitalWallet({
293 buttonId: 'apple-pay-button',
294 type: 'applePay',
295 readyRef: applePayReady,
296 })
297 }
298
299 const squareTokenize = async (payments, totalAmount) => {
300 try {
301 const { token, status, errors } = await payments.tokenize({
302 amount: totalAmount.toString(),
303 billingContact: {
304 familyName: bookingData.data.bookings[0].customer.lastName,
305 givenName: bookingData.data.bookings[0].customer.firstName,
306 email: bookingData.data.bookings[0].customer.email,
307 phone: bookingData.data.bookings[0].customer.phone,
308 },
309 customerInitiated: true,
310 sellerKeyedIn: false,
311 currencyCode: bookingData.data.payment.currency,
312 intent: 'CHARGE',
313 })
314
315 if (status === 'OK') {
316 return token
317 } else if (status === 'Invalid' && errors.length > 0) {
318 const messages = errors.map((err) => err.message)
319 emits('payment-error', messages.join(', '))
320 useScrollTo(paymentStepRef.value, paymentStepRef.value, 20, 300)
321 return ''
322 }
323 } catch (e) {
324 console.log(e)
325 }
326 }
327
328 const createSquarePayment = async (token) => {
329 if (!token) {
330 return
331 }
332 useCreateBooking(
333 store,
334 useBookingData(
335 store,
336 null,
337 false,
338 {
339 locationId: squareLocationId,
340 sourceId: token,
341 idempotencyKey: window.crypto.randomUUID(),
342 },
343 null,
344 ),
345 function (response) {
346 successBooking(response)
347 },
348 (response) => {
349 errorBooking(response)
350 },
351 )
352 }
353
354 function successBooking(response) {
355 useCreateBookingSuccess(store, response, function () {
356 nextStep()
357 })
358 }
359
360 function getSquareBookingErrorResponse(error) {
361 if (error?.response?.data) {
362 return error.response.data
363 }
364
365 if (error?.data) {
366 return error
367 }
368
369 const labels = amLabels.value || amLabels
370
371 return {
372 data: {
373 message: error?.message || labels.payment_error,
374 },
375 }
376 }
377
378 function errorBooking(error) {
379 useCreateBookingError(store, getSquareBookingErrorResponse(error), () => {
380 emits('payment-error', getErrorMessage())
381 })
382 }
383 </script>
384
385 <script>
386 export default {
387 name: 'PaymentSquare',
388 }
389 </script>
390
391 <style lang="scss">
392 .amelia-v2-booking {
393 #amelia-container {
394 .am-fs__square-loading {
395 display: flex;
396 flex-direction: column;
397 align-items: center;
398 justify-content: center;
399 width: 100%;
400
401 .el-skeleton {
402 display: flex;
403 align-items: center;
404 flex-direction: column;
405 gap: 10px;
406 width: 100%;
407
408 &__item {
409 width: 100%;
410 height: 40px;
411 }
412 }
413 }
414
415 .am-fs__payment-square {
416 display: flex;
417 flex-direction: column;
418 gap: 6px;
419
420 span {
421 display: inline-flex;
422 font-size: 14px;
423 font-weight: 500;
424 line-height: 1.42857;
425 color: #6772e5;
426 }
427
428 &__google-pay {
429 #google-pay-button {
430 width: 100%;
431 div {
432 button {
433 width: 100%;
434 display: flex;
435 justify-content: center;
436 align-items: center;
437 }
438 }
439 }
440 }
441
442 // Apple Pay button styling
443 &__apple-pay {
444 #apple-pay-button {
445 height: 40px;
446 width: 100%;
447 font-size: 15px;
448 display: inline-block;
449 -webkit-appearance: -apple-pay-button;
450 -apple-pay-button-type: plain;
451 -apple-pay-button-style: black;
452 }
453 }
454
455 #card-container {
456 .sq-card-iframe-container {
457 border: 1px solid #d9d9d9;
458 }
459 }
460
461 .am-fs__payment-divider {
462 display: flex;
463 align-items: center;
464 justify-content: center;
465 width: 100%;
466 margin: 16px 0;
467 text-align: center;
468 }
469
470 .am-fs__payment-divider::before,
471 .am-fs__payment-divider::after {
472 content: '';
473 flex-grow: 1;
474 height: 1px;
475 background-color: var(--am-c-pay-text-op60);
476 margin: 0 8px;
477 }
478
479 .am-divider-text {
480 font-size: 14px;
481 color: var(--am-c-pay-text-op60);
482 text-transform: uppercase;
483 line-height: 1.33333;
484 font-weight: 500;
485 }
486 }
487 }
488 }
489 </style>
490