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