PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
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 / StepForm / InfoStep / InfoStep.vue
ameliabooking / v3 / src / views / public / StepForm / InfoStep Last commit date
parts 3 weeks ago InfoStep.vue 3 weeks ago
InfoStep.vue
1060 lines
1 <template>
2 <div ref="infoFormWrapperRef" class="am-fs__info" :class="props.globalClass">
3 <div v-show="!loading">
4 <div
5 v-if="paymentError && (instantBooking || isWaitingListBooking)"
6 class="am-fs__info-error"
7 >
8 <AmAlert type="error" :title="paymentError" :show-icon="true" :closable="false"> </AmAlert>
9 </div>
10
11 <div v-if="authError" class="am-fs__info-error">
12 <AmAlert type="error" :title="authErrorMessage" :show-icon="true" :closable="true">
13 </AmAlert>
14 </div>
15
16 <!-- Social Buttons -->
17 <div
18 v-if="
19 (settings.socialLogin.googleLoginEnabled &&
20 settings.general.googleClientId &&
21 !loggedInUser) ||
22 (settings.socialLogin.facebookLoginEnabled &&
23 settings.socialLogin.facebookCredentialsEnabled &&
24 !loggedInUser)
25 "
26 >
27 <div class="am-fs__info-social-wrapper">
28 <div class="am-fs__info-social-wrapper__label">
29 {{ amLabels.auto_fill_your_details }}
30 </div>
31 <am-social-button :provider="socialProvider" @social-action="onSignupSocial" />
32 </div>
33
34 <!-- Social Divider -->
35 <div class="am-fs__info-social-divider">
36 <span class="par-sm">{{ amLabels.or_enter_details_below }}</span>
37 </div>
38 <!-- /Social Divider -->
39 </div>
40 <!-- /Social Buttons -->
41 <el-form
42 ref="infoFormRef"
43 :model="infoFormData"
44 :rules="rules"
45 label-position="top"
46 class="am-fs__info-form"
47 :class="[
48 { 'am-fs__info-form-mobile': pageWidth < 330 },
49 { 'am-fs__info-form-mobile-s': pageWidth < 300 },
50 ]"
51 >
52 <template v-for="item in amCustomize.infoStep.order" :key="item.id">
53 <component
54 :is="formFields[item.id].template"
55 ref="primeCollectorRef"
56 v-model="infoFormData[item.id]"
57 v-model:country-code="formFields[item.id].countryCode"
58 v-bind="formFields[item.id].props"
59 v-on="'handlers' in formFields[item.id] ? formFields[item.id].handlers : {}"
60 ></component>
61 </template>
62
63 <el-form-item
64 v-if="
65 settings.featuresIntegrations.mailchimp.enabled &&
66 settings.mailchimp.subscribeFieldVisible &&
67 amCustomize.infoStep.options.email.visibility
68 "
69 class="am-subscribe"
70 >
71 <AmCheckBox v-model="subscribeToMailchimp" :label="amLabels.subscribe_to_mailing_list">
72 </AmCheckBox>
73 </el-form-item>
74
75 <!-- Custom Fields TODO - validation for custom fields isn't set-->
76 <template v-if="availableCustomFields && allCustomFields">
77 <el-form-item
78 v-for="(cf, index) in allCustomFields"
79 v-show="cf.id in availableCustomFields && checkCustomerCustomFieldVisibility(cf)"
80 :id="'am-cf-' + cf.id"
81 :ref="(el) => (customFieldsRefs[index] = el)"
82 :key="index"
83 class="am-fs__info-form__item"
84 :class="[
85 { 'is-required': cf.type === 'file' && cf.required },
86 `am-cf-width-${cf.width}`,
87 { 'am-rtl': isRtl },
88 ]"
89 label-position="top"
90 :prop="cf.required && cf.type !== 'content' ? 'cf' + cf.id : 'inputFile'"
91 >
92 <!-- ####### LABEL ####### -->
93 <template v-if="cf.type !== 'content'" #label>
94 <span
95 v-if="(cf.type === 'checkbox' || cf.type === 'radio') && cf.label"
96 :class="
97 (cf.type === 'checkbox' || cf.type === 'radio') && cf.required
98 ? 'am-custom-required-as-html'
99 : ''
100 "
101 v-html="cf.label ? cf.label : ''"
102 >
103 </span>
104 <span v-else class="am-fs__info-form__label">
105 {{ cf.label }}
106 </span>
107 </template>
108 <!-- ####### /LABEL ####### -->
109
110 <!-- ####### INPUT ####### -->
111 <!-- types - [input, text-area] -->
112 <component
113 :is="customFieldsComponents[cf.type]"
114 v-model="infoFormData['cf' + cf.id]"
115 :type="cf.type === 'text-area' ? 'textarea' : cf.type === 'text' ? 'text' : ''"
116 :placeholder="refCFPlaceholders[cf.id] && refCFPlaceholders[cf.id].placeholder"
117 ></component>
118 <!-- /types - [input, text-area] -->
119
120 <!-- Address Field -->
121 <template v-if="cf.type === 'address'">
122 <AmAddressInput
123 :id="`amelia-address-autocomplete-${cf.id}`"
124 v-model="infoFormData['cf' + cf.id]"
125 @address-selected="(address) => addressSelected(address, cf.id)"
126 />
127 </template>
128 <!-- /Address Field -->
129
130 <!-- type - date-picker-full -->
131 <AmDatePickerFull
132 v-if="cf.type === 'datepicker'"
133 :persistent="false"
134 :existing-date="infoFormData['cf' + cf.id]"
135 :disabled="false"
136 @selected-date="
137 (dateString) => {
138 selectedDatePickerValue('cf' + cf.id, dateString)
139 }
140 "
141 />
142 <!-- /type - date-picker-full -->
143
144 <!-- type - select -->
145 <AmSelect
146 v-if="cf.type === 'select'"
147 v-model="infoFormData['cf' + cf.id]"
148 :fit-input-width="true"
149 >
150 <AmOption
151 v-for="(option, i) in cf.options"
152 :key="i"
153 :label="option.label"
154 :value="option.label"
155 />
156 </AmSelect>
157 <!-- /type - select -->
158
159 <!-- type - radio -->
160 <AmRadioGroup v-if="cf.type === 'radio'" v-model="infoFormData['cf' + cf.id]">
161 <AmRadio
162 v-for="(option, i) in cf.options"
163 :key="i"
164 :label="option.label"
165 :value="option.label"
166 />
167 </AmRadioGroup>
168 <!-- /type - radio -->
169
170 <!-- type - checkbox -->
171 <AmCheckBoxGroup v-if="cf.type === 'checkbox'" v-model="infoFormData['cf' + cf.id]">
172 <AmCheckBox
173 v-for="(option, i) in cf.options"
174 :key="i"
175 :label="option.label"
176 :value="option.label"
177 />
178 </AmCheckBoxGroup>
179 <!-- /type - checkbox -->
180
181 <!-- type - attachment -->
182 <AmAttachment
183 v-if="cf.type === 'file'"
184 :id="cf.id"
185 v-model="infoFormData['cf' + cf.id]"
186 :auto-upload="false"
187 :accept="customFieldsAllowedExtensions"
188 @change="onAddFile"
189 @remove="onRemoveFile"
190 >
191 {{ amLabels.upload_file_here }}
192 </AmAttachment>
193 <!-- /type - attachment -->
194
195 <!-- type - content -->
196 <div v-if="cf.type === 'content'" v-html="cf.label"></div>
197 <!-- /type - content -->
198 <!-- ####### INPUT ####### -->
199 </el-form-item>
200 </template>
201 <div
202 v-if="
203 instantBooking &&
204 settings.payments.wc.enabled &&
205 !settings.payments.wc.onSiteIfFree &&
206 wcEntityEnabled
207 "
208 class="am-fs__payments-sentence"
209 >
210 <p>
211 {{ amLabels.payment_wc_mollie_sentence }}
212 </p>
213 </div>
214 </el-form>
215
216 <PaymentOnSite
217 v-if="
218 isWaitingListBooking ||
219 (instantBooking &&
220 (settings.payments.wc.enabled
221 ? settings.payments.wc.onSiteIfFree || !wcEntityEnabled
222 : true))
223 "
224 ref="refOnSiteBooking"
225 :instant-booking="instantBooking"
226 @payment-error="callPaymentError"
227 />
228
229 <PaymentWc
230 v-if="
231 instantBooking &&
232 !isWaitingListBooking &&
233 settings.payments.wc.enabled &&
234 !settings.payments.wc.onSiteIfFree &&
235 wcEntityEnabled
236 "
237 ref="refWcBooking"
238 :instant-booking="instantBooking"
239 @payment-error="callPaymentError"
240 />
241 </div>
242
243 <BookingSkeleton />
244 </div>
245 </template>
246
247 <script setup>
248 import FirstNameFormField from './parts/FirstNameFormField.vue'
249 import LastNameFormField from './parts/LastNameFormField.vue'
250 import EmailFormField from './parts/EmailFormField.vue'
251 import PhoneFormField from './parts/PhoneFormField.vue'
252
253 import AmAlert from '../../../_components/alert/AmAlert'
254 import AmInput from '../../../_components/input/AmInput.vue'
255 import AmAttachment from '../../../_components/attachment/AmAttachment.vue'
256 import AmCheckBoxGroup from '../../../_components/checkbox/AmCheckboxGroup.vue'
257 import AmCheckBox from '../../../_components/checkbox/AmCheckbox.vue'
258 import AmRadioGroup from '../../../_components/radio/AmRadioGroup.vue'
259 import AmRadio from '../../../_components/radio/AmRadio.vue'
260 import AmDatePickerFull from '../../../_components/date-picker-full/AmDatePickerFull.vue'
261 import AmSelect from '../../../_components/select/AmSelect.vue'
262 import AmOption from '../../../_components/select/AmOption.vue'
263 import AmAddressInput from '../../../_components/address-input/AmAddressInput.vue'
264
265 import PaymentOnSite from '../../Parts/Payment/PaymentOnSite.vue'
266 import PaymentWc from '../../Parts/Payment/PaymentWc.vue'
267 import BookingSkeleton from '../../Parts/BookingSkeleton.vue'
268 import moment from 'moment'
269 import {
270 ref,
271 computed,
272 inject,
273 provide,
274 reactive,
275 markRaw,
276 onMounted,
277 watchEffect,
278 watch,
279 nextTick,
280 } from 'vue'
281 import { useStore } from 'vuex'
282 import VueAuthenticate from 'vue-authenticate'
283 import { settings } from '../../../../plugins/settings'
284 import { usePrepaidPrice, usePaymentError } from '../../../../assets/js/common/appointments'
285 import { useScrollTo } from '../../../../assets/js/common/scrollElements'
286 import { useCustomFields } from '../../../../assets/js/public/customFields'
287 import useAction from '../../../../assets/js/public/actions'
288 import { useElementSize } from '@vueuse/core'
289 import { useCartHasItems } from '../../../../assets/js/public/cart'
290 import { announceFormErrors, useLiveAnnouncer } from '../../../../assets/js/common/liveAnnouncer'
291 import httpClient from '../../../../plugins/axios'
292 import AmSocialButton from '../../../common/FormFields/AmSocialButton.vue'
293 import { SocialAuthOptions } from '../../../../assets/js/admin/socialAuthOptions'
294 import { mapAddressComponentsForXML } from '../../../../assets/js/common/helper'
295 import { useIvyMapping } from '../../../../assets/js/public/ivy'
296 import { getInternationalPhoneValue } from '../../../../assets/js/common/phone'
297
298 let props = defineProps({
299 globalClass: {
300 type: String,
301 default: '',
302 },
303 })
304
305 // * Store
306 let store = useStore()
307
308 // * Labels
309 const amLabels = inject('amLabels')
310
311 // * Customize
312 let amCustomize = inject('amCustomize')
313
314 const { announceAlert } = useLiveAnnouncer()
315
316 // * Shortcode data
317 const shortcodeData = inject('shortcodeData')
318
319 let infoFormWrapperRef = ref(null)
320 let { width: pageWidth } = useElementSize(infoFormWrapperRef)
321
322 let allFieldsRefs = ref([])
323 let primeCollectorRef = ref([])
324 let customFieldsRefs = ref([])
325
326 let customFieldsComponents = reactive({
327 text: markRaw(AmInput),
328 'text-area': markRaw(AmInput),
329 })
330
331 useCustomFields(store)
332
333 // * Get custom fields
334 let availableCustomFields = computed(() => store.getters['booking/getAvailableCustomFields'])
335 let allCustomFields = computed(() => store.getters['entities/getCustomFields'])
336
337 // * Step Functions
338 const { nextStep, footerButtonReset, footerButtonClicked, headerButtonPreviousClicked } = inject(
339 'changingStepsFunctions',
340 {
341 nextStep: () => {},
342 headerButtonPreviousClick: () => {},
343 footerButtonReset: () => {},
344 footerButtonClicked: {
345 value: false,
346 },
347 headerButtonPreviousClicked: {
348 value: false,
349 },
350 },
351 )
352
353 watch(headerButtonPreviousClicked, () => {
354 if (instantBooking.value && !isWaitingListBooking.value) {
355 addPaymentsStep()
356 }
357 })
358
359 let { addPaymentsStep } = inject('addPaymentsStep', {
360 addPaymentsStep: () => {},
361 })
362
363 let { removePaymentsStep } = inject('removePaymentsStep', {
364 removePaymentsStep: () => {},
365 })
366
367 function callPaymentError(msg) {
368 usePaymentError(store, msg)
369 }
370
371 let paymentError = computed(() => store.getters['booking/getError'])
372
373 /**
374 * Form Block start
375 */
376 // * Form reference
377 let infoFormRef = ref(null)
378
379 let isRtl = computed(() => store.getters['getIsRtl'])
380
381 let phoneError = ref(false)
382 let isPhoneValid = ref(false)
383 let refreshPhoneComponent = ref(0)
384
385 let loggedInUser = computed(
386 () =>
387 (store.getters['booking/getCustomerId'] && store.getters['booking/getCustomerEmail']) ||
388 (!!window.ameliaUser && window.ameliaUser.type == 'admin'),
389 )
390
391 // * Form data
392 let infoFormData = ref({
393 firstName: computed({
394 get: () => store.getters['booking/getCustomerFirstName'],
395 set: (val) => {
396 store.commit('booking/setCustomerFirstName', val ? val : '')
397 },
398 }),
399 lastName: computed({
400 get: () => store.getters['booking/getCustomerLastName'],
401 set: (val) => {
402 store.commit('booking/setCustomerLastName', val ? val : '')
403 },
404 }),
405 email: computed({
406 get: () => store.getters['booking/getCustomerEmail'],
407 set: (val) => {
408 store.commit('booking/setCustomerEmail', val ? val : '')
409 },
410 }),
411 phone: computed({
412 get: () => store.getters['booking/getCustomerPhone'],
413 set: (val) => {
414 store.commit('booking/setCustomerPhone', val ? val : '')
415 },
416 }),
417 })
418 provide('infoFormData', infoFormData)
419
420 // * Form Fields Object
421 let formFields = ref({
422 firstName: {
423 template: markRaw(FirstNameFormField),
424 props: {
425 class: computed(() => (isRtl.value ? 'am-rtl' : '')),
426 },
427 },
428 lastName: {
429 template: markRaw(LastNameFormField),
430 props: {
431 class: computed(() => (isRtl.value ? 'am-rtl' : '')),
432 },
433 },
434 email: {
435 template: markRaw(EmailFormField),
436 props: {
437 class: computed(() => (isRtl.value ? 'am-rtl' : '')),
438 },
439 },
440 phone: {
441 countryCode: computed({
442 get: () => {
443 const iso = store.getters['booking/getCustomerCountryPhoneIso']
444 return iso ? iso.toUpperCase() : ''
445 },
446 set: (val) => {
447 store.commit('booking/setCustomerCountryPhoneIso', val ? val.toLowerCase() : '')
448 },
449 }),
450 template: markRaw(PhoneFormField),
451 props: {
452 class: computed(() => (isRtl.value ? 'am-rtl' : '')),
453 phoneError: computed(() => phoneError.value && !isPhoneValid.value),
454 errorMessage: computed(() =>
455 !isPhoneValid.value && infoFormData.value.phone
456 ? amLabels.value.enter_valid_phone_warning
457 : '',
458 ),
459 refreshTrigger: computed(() => refreshPhoneComponent.value),
460 },
461 handlers: {
462 handlePhoneData: (phoneData) => {
463 // If phone data is received without country code, set the default country code
464 if (
465 phoneData &&
466 !phoneData.countryCode &&
467 !store.getters['booking/getCustomerCountryPhoneIso'] &&
468 settings.general.phoneDefaultCountryCode !== 'auto'
469 ) {
470 store.commit(
471 'booking/setCustomerCountryPhoneIso',
472 settings.general.phoneDefaultCountryCode.toLowerCase(),
473 )
474 }
475
476 // Keep international format so the phone includes the country calling code.
477 const countryCode = (
478 phoneData?.countryCode ||
479 store.getters['booking/getCustomerCountryPhoneIso'] ||
480 ''
481 ).toUpperCase()
482
483 store.commit(
484 'booking/setCustomerPhone',
485 getInternationalPhoneValue(
486 phoneData?.phoneNumber ?? phoneData?.e164,
487 phoneData,
488 countryCode,
489 ),
490 )
491
492 isPhoneValid.value = !!phoneData.isValid
493 },
494 },
495 },
496 })
497
498 let subscribeToMailchimp = computed({
499 get: () => store.getters['booking/getCustomerSubscribe'],
500 set: (val) => {
501 store.commit('booking/setCustomerSubscribe', val)
502 },
503 })
504
505 // * Form validation rules
506 let rules = ref({
507 firstName: [
508 {
509 required: true,
510 message: amLabels.value.enter_first_name_warning,
511 trigger: 'submit',
512 },
513 ],
514 lastName: [
515 {
516 required: amCustomize.infoStep.options.lastName.required,
517 message: amLabels.value.enter_last_name_warning,
518 trigger: 'submit',
519 },
520 ],
521 email: [
522 {
523 required: amCustomize.infoStep.options.email.required,
524 message: amLabels.value.enter_valid_email_warning,
525 trigger: 'submit',
526 },
527 {
528 type: 'email',
529 message: amLabels.value.enter_valid_email_warning,
530 trigger: 'submit',
531 },
532 ],
533 phone: [
534 {
535 required: amCustomize.infoStep.options.phone.required,
536 message: amLabels.value.enter_phone_warning,
537 trigger: ['blur', 'submit'],
538 },
539 ],
540 })
541
542 let refOnSiteBooking = ref(null)
543
544 let refWcBooking = ref(null)
545
546 // * InitInfoStep hook - custom fields placeholder
547 let refCFPlaceholders = ref({})
548
549 // * InitInfoStep hook - adding coupon
550 let couponCode = ref('')
551
552 let loading = computed(() => store.getters['booking/getLoading'])
553
554 let instantBooking = ref(usePrepaidPrice(store) === 0)
555
556 let isWaitingListBooking = computed(
557 () => store.getters['appointmentWaitingListOptions/getIsWaitingListSlot'],
558 )
559
560 allCustomFields.value.forEach((customField) => {
561 if (customField.id in availableCustomFields.value) {
562 rules.value['cf' + customField.id] = [
563 {
564 message: amLabels.value.required_field,
565 required: customField.required,
566 trigger: 'submit',
567 },
568 ]
569 }
570 })
571
572 function selectedDatePickerValue(id, dateString) {
573 infoFormData.value[id] = moment(dateString, 'YYYY-MM-DD').toDate()
574 }
575
576 function onAddFile(a) {
577 store.commit('booking/setAttachment', {
578 id: a.id,
579 raw: a.raw,
580 })
581
582 infoFormData.value['cf' + a.id] = a.raw
583 }
584
585 function onRemoveFile(a) {
586 store.commit('booking/setAttachment', {
587 id: a.id,
588 raw: a.raw,
589 })
590
591 infoFormData.value['cf' + a.id] = a.raw
592 }
593
594 function addressSelected(addressComponents, cfId) {
595 if (addressComponents && store.state.booking.appointment.bookings[0].customFields[cfId]) {
596 store.state.booking.appointment.bookings[0].customFields[cfId].components =
597 mapAddressComponentsForXML(addressComponents)
598 }
599 }
600
601 let customFieldsAllowedExtensions = ref('')
602
603 /**
604 * Submit Form Function
605 */
606 function submitForm() {
607 footerButtonReset()
608
609 // Trim inputs
610 infoFormData.value.firstName = infoFormData.value.firstName.trim()
611 infoFormData.value.lastName = infoFormData.value.lastName.trim()
612 infoFormData.value.email = infoFormData.value.email.trim()
613
614 useAction(
615 store,
616 { rules: rules.value },
617 'customValidation',
618 store.getters['booking/getBookableType'],
619 null,
620 null,
621 )
622 infoFormRef.value.validate((valid) => {
623 if (valid && (amCustomize.infoStep.options.phone.required ? isPhoneValid.value : true)) {
624 phoneError.value = false
625 if (!instantBooking.value && !isWaitingListBooking.value) {
626 nextStep()
627 } else {
628 // For waiting list bookings always use onSite gateway.
629 if (isWaitingListBooking.value) {
630 store.commit('booking/setPaymentGateway', 'onSite')
631 if (
632 refOnSiteBooking.value &&
633 typeof refOnSiteBooking.value.continueWithBooking === 'function'
634 ) {
635 refOnSiteBooking.value.continueWithBooking()
636 } else {
637 // Fallback: remove payment step already, just proceed to next step if exposed
638 nextStep()
639 }
640 } else {
641 if (
642 settings.payments.wc.enabled &&
643 !settings.payments.wc.onSiteIfFree &&
644 wcEntityEnabled.value
645 ) {
646 store.commit('booking/setPaymentGateway', 'wc')
647 refWcBooking.value.continueWithBooking()
648 } else {
649 store.commit('booking/setPaymentGateway', 'onSite')
650 if (
651 refOnSiteBooking.value &&
652 typeof refOnSiteBooking.value.continueWithBooking === 'function'
653 ) {
654 refOnSiteBooking.value.continueWithBooking()
655 } else {
656 nextStep()
657 }
658 }
659 }
660 }
661 } else {
662 // * Scroll to the first error field
663 let fieldElement
664
665 let phoneField = infoFormRef.value.fields.find((el) => el.prop === 'phone')
666 let shouldFlagPhone =
667 phoneField &&
668 amCustomize.infoStep.options.phone.visibility &&
669 (amCustomize.infoStep.options.phone.required || !!infoFormData.value.phone) &&
670 !isPhoneValid.value
671
672 if (shouldFlagPhone) {
673 phoneField.validateState = 'error'
674 }
675 phoneError.value = !!(phoneField && phoneField.validateState === 'error')
676
677 nextTick(() => {
678 infoFormRef.value.fields.some((el) => {
679 if (el.validateState === 'error') {
680 fieldElement = el.$el
681 return el.validateState === 'error'
682 }
683 })
684
685 useScrollTo(infoFormWrapperRef.value, fieldElement, 20, 300)
686 })
687
688 announceFormErrors(infoFormRef, announceAlert)
689
690 return false
691 }
692 })
693 }
694
695 let socialProvider = ref('')
696 const VueAuthenticateInstance = VueAuthenticate.factory(httpClient, SocialAuthOptions)
697
698 // * Facebook Sign in error alert
699 let authError = ref(false)
700 let authErrorMessage = ref('')
701
702 function onSignupSocial({ provider, credentials }) {
703 const socialCheckUrl = `/users/authentication/${provider}`
704 const data = {}
705 socialProvider = provider
706
707 if (provider === 'google') {
708 data.code = credentials
709 httpClient.post(`${socialCheckUrl}`, data).then((response) => {
710 setDataFromSocialLogin(response.data.data.user)
711 })
712 }
713 if (provider === 'facebook') {
714 VueAuthenticateInstance.options.providers[provider].url = `${socialCheckUrl}`
715 VueAuthenticateInstance.authenticate(provider, data)
716 .then((response) => {
717 setDataFromSocialLogin(response.data.data.user)
718 })
719 .catch((error) => {
720 if (!VueAuthenticateInstance.isAuthenticated()) {
721 authError.value = true
722 authErrorMessage.value = 'User is not authenticated.'
723 store.commit('setLoading', false)
724 }
725 })
726 }
727 }
728
729 function setDataFromSocialLogin(data) {
730 infoFormData.value.firstName = data.firstName
731 infoFormData.value.lastName = data.lastName
732 infoFormData.value.email = data.email
733 if (data.phone) {
734 infoFormData.value.phone = data.phone
735 const normalizedIso = data.countryPhoneIso ? data.countryPhoneIso.trim().toLowerCase() : ''
736 store.commit('booking/setCustomerCountryPhoneIso', normalizedIso)
737 refreshPhoneComponent.value++
738 }
739 }
740
741 // * Watching when footer button was clicked
742 watchEffect(() => {
743 if (footerButtonClicked.value) {
744 submitForm()
745 }
746 })
747
748 let visibilityFlags = ref({})
749
750 function checkCustomerCustomFieldVisibility(cf) {
751 if (
752 cf.saveType === 'customer' &&
753 loggedInUser.value &&
754 store.state.booking.appointment.bookings[0].customer.customFields
755 ) {
756 let customerCustomFields = store.state.booking.appointment.bookings[0].customer.customFields
757
758 try {
759 let parsedCustomerCF = JSON.parse(customerCustomFields)
760 let af = availableCustomFields.value[cf.id]
761 let currentValue = af ? af.value : undefined
762 let alreadyFlagged = Object.prototype.hasOwnProperty.call(visibilityFlags.value, cf.id)
763
764 if (!(cf.id in parsedCustomerCF)) {
765 return true
766 }
767
768 if (alreadyFlagged) {
769 return visibilityFlags.value[cf.id]
770 }
771
772 // Always show when saveFirstChoice is false
773 if (!cf.saveFirstChoice) {
774 visibilityFlags.value[cf.id] = true
775 return true
776 }
777
778 // When saveFirstChoice is true, show if saved value is empty
779 let visible
780 switch (cf.type) {
781 case 'checkbox':
782 case 'file':
783 visible = Array.isArray(currentValue) ? currentValue.length === 0 : currentValue == null
784 break
785 default:
786 visible = currentValue == null || currentValue === ''
787 }
788
789 visibilityFlags.value[cf.id] = visible
790 return visible
791 } catch (e) {
792 return true
793 }
794 }
795
796 return true
797 }
798
799 let addressCustomFields = ref([])
800
801 let wcEntityEnabled = ref(false)
802
803 onMounted(() => {
804 if (infoFormWrapperRef.value) {
805 infoFormWrapperRef.value.focus()
806 }
807
808 let entity = null
809
810 switch (store.getters['booking/getBookableType']) {
811 case 'appointment':
812 entity =
813 useCartHasItems(store) === 1
814 ? store.getters['entities/getBookableFromBookableEntities'](
815 store.getters['booking/getSelection'],
816 )
817 : null
818
819 break
820
821 case 'package':
822 entity = store.getters['entities/getPackage'](store.getters['booking/getPackageId'])
823
824 break
825 }
826
827 let entityPayments = entity && entity.settings ? JSON.parse(entity.settings)['payments'] : null
828
829 wcEntityEnabled.value =
830 entityPayments && 'wc' in entityPayments
831 ? !('enabled' in entityPayments.wc) || entityPayments.wc.enabled
832 : settings.payments.wc.enabled
833
834 if (settings.general.customFieldsAllowedExtensions) {
835 customFieldsAllowedExtensions.value = Object.keys(
836 settings.general.customFieldsAllowedExtensions,
837 ).join(', ')
838 }
839
840 Object.keys(availableCustomFields.value).forEach((id) => {
841 infoFormData.value['cf' + id] = computed({
842 get: () => store.state.booking.appointment.bookings[0].customFields[id].value,
843 set: (val) => {
844 store.state.booking.appointment.bookings[0].customFields[id].value = val
845 },
846 })
847
848 // * Placeholder implementation for custom input and textarea
849 if (
850 availableCustomFields.value[id].type === 'text' ||
851 availableCustomFields.value[id].type === 'text-area'
852 ) {
853 refCFPlaceholders.value[id] = { placeholder: '' }
854 }
855 })
856
857 addressCustomFields.value.forEach((el) => {
858 let id = el.id.split('amelia-address-autocomplete-')
859 if (
860 id &&
861 id.length > 1 &&
862 id[1] in store.state.booking.appointment.bookings[0].customFields &&
863 store.state.booking.appointment.bookings[0].customFields[id[1]].value
864 ) {
865 el.update(store.state.booking.appointment.bookings[0].customFields[id[1]].value)
866 }
867 })
868
869 if (instantBooking.value || isWaitingListBooking.value) {
870 removePaymentsStep()
871 }
872
873 nextTick(() => {
874 primeCollectorRef.value.forEach((el) => {
875 if (el.primeFieldRef) {
876 allFieldsRefs.value.push(el.primeFieldRef)
877 }
878 })
879 allFieldsRefs.value.push.apply(allFieldsRefs.value, customFieldsRefs.value)
880 })
881
882 useAction(
883 store,
884 { customFieldsPlaceholders: refCFPlaceholders, couponCode },
885 'InitInfoStep',
886 store.getters['booking/getBookableType'],
887 null,
888 null,
889 )
890
891 if (couponCode.value) {
892 store.commit('booking/setCouponCode', couponCode.value)
893 }
894
895 if (useIvyMapping(store, shortcodeData, infoFormData)) {
896 refreshPhoneComponent.value++
897 }
898 })
899 </script>
900
901 <script>
902 export default {
903 name: 'InfoStep',
904 key: 'infoStep',
905 inheritAttrs: false,
906 sidebarData: {
907 label: 'info_step',
908 icon: 'user',
909 stepSelectedData: [],
910 finished: false,
911 selected: false,
912 },
913 }
914 </script>
915
916 <style lang="scss">
917 .amelia-v2-booking #amelia-container {
918 .am-fs {
919 &__main {
920 &-content.am-fs__info {
921 padding-top: 20px;
922 }
923
924 &-inner {
925 overflow: hidden;
926 }
927 }
928
929 &__info {
930 &-error {
931 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{100}ms am-animation-slide-up;
932 animation-fill-mode: both;
933 margin-bottom: 10px;
934 }
935
936 &-social-wrapper {
937 display: flex;
938 align-items: center;
939 flex-direction: column;
940 width: 100%;
941 margin-bottom: 24px;
942 gap: 24px;
943
944 .am-social-signin {
945 &__google {
946 #g_id_onload {
947 display: none;
948 }
949 .g_id_signin {
950 width: 64px;
951 }
952 }
953 }
954
955 &__label {
956 font-weight: 500;
957 font-size: 15px;
958 }
959 }
960
961 &-social-divider {
962 align-items: center;
963 display: flex;
964 margin-bottom: 24px;
965
966 // Before & After
967 &:before,
968 &:after {
969 background: var(--shade-250, #d1d5d7);
970 content: '';
971 height: 1px;
972 width: 100%;
973 }
974
975 span {
976 flex: none;
977 font-size: 15px;
978 font-style: normal;
979 font-weight: 400;
980 line-height: 24px;
981 color: var(--shade-500, #808a90);
982 margin-left: 8px;
983 margin-right: 8px;
984 }
985 }
986
987 &-form {
988 display: flex;
989 flex-wrap: wrap;
990 justify-content: space-between;
991
992 & > * {
993 $count: 100;
994 @for $i from 0 through $count {
995 &:nth-child(#{$i + 1}) {
996 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up;
997 animation-fill-mode: both;
998 }
999 }
1000 }
1001
1002 .am-fs__payments-sentence {
1003 width: 100%;
1004 }
1005
1006 .el-form-item {
1007 width: calc(50% - 12px);
1008
1009 &.am-cf-width-100 {
1010 width: 100%;
1011 }
1012
1013 &.am-subscribe {
1014 width: 100%;
1015 .el-checkbox {
1016 &__input {
1017 height: 32px;
1018 line-height: 32px;
1019 align-items: center;
1020 }
1021
1022 &__label {
1023 line-height: 32px;
1024 align-items: center;
1025 }
1026 }
1027 }
1028 }
1029
1030 &-mobile {
1031 gap: 12px 6px;
1032 .el-form-item {
1033 width: 100%;
1034 }
1035 &-s {
1036 gap: 0px;
1037 }
1038 }
1039
1040 &__label {
1041 display: inline-block;
1042 color: var(--am-c-main-text);
1043 font-family: var(--am-font-family);
1044 font-weight: 500;
1045 margin-bottom: 4px;
1046 }
1047
1048 .el-form-item__label {
1049 line-height: unset;
1050 padding: 0;
1051 }
1052 }
1053 }
1054 }
1055 }
1056 .pac-container {
1057 z-index: 9999999991 !important;
1058 }
1059 </style>
1060