diff --git a/app/javascript/packs/widget.js b/app/javascript/packs/widget.js index 6eab2db8b..7b11d05e3 100644 --- a/app/javascript/packs/widget.js +++ b/app/javascript/packs/widget.js @@ -7,9 +7,14 @@ import store from '../widget/store'; import App from '../widget/App.vue'; import ActionCableConnector from '../widget/helpers/actionCable'; import i18n from '../widget/i18n'; -import { isPhoneE164OrEmpty } from 'shared/helpers/Validators'; +import { + startsWithPlus, + isPhoneNumberValidWithDialCode, +} from 'shared/helpers/Validators'; import router from '../widget/router'; import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer'; +const PhoneInput = () => import('../widget/components/Form/PhoneInput'); + Vue.use(VueI18n); Vue.use(Vuelidate); Vue.use(VueDOMPurifyHTML, domPurifyConfig); @@ -19,8 +24,18 @@ const i18nConfig = new VueI18n({ messages: i18n, }); Vue.use(VueFormulate, { + library: { + phoneInput: { + classification: 'number', + component: PhoneInput, + slotProps: { + component: ['placeholder', 'hasErrorInPhoneInput'], + }, + }, + }, rules: { - isPhoneE164OrEmpty: ({ value }) => isPhoneE164OrEmpty(value), + startsWithPlus: ({ value }) => startsWithPlus(value), + isValidPhoneNumber: ({ value }) => isPhoneNumberValidWithDialCode(value), }, classes: { outer: 'mb-4 wrapper', diff --git a/app/javascript/shared/components/FluentIcon/icons.json b/app/javascript/shared/components/FluentIcon/icons.json index 28178fef2..4e8f6ff2b 100644 --- a/app/javascript/shared/components/FluentIcon/icons.json +++ b/app/javascript/shared/components/FluentIcon/icons.json @@ -3,11 +3,13 @@ "arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z", "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", "checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z", + "chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z", "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", "chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z", "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", "document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z", "emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", + "globe-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999ZM14.939 16.5H9.06c.652 2.414 1.786 4.002 2.939 4.002s2.287-1.588 2.939-4.002Zm-7.43 0H4.785a8.532 8.532 0 0 0 4.094 3.411c-.522-.82-.953-1.846-1.27-3.015l-.102-.395Zm11.705 0h-2.722c-.324 1.335-.792 2.5-1.373 3.411a8.528 8.528 0 0 0 3.91-3.127l.185-.283ZM7.094 10H3.735l-.005.017a8.525 8.525 0 0 0-.233 1.984c0 1.056.193 2.067.545 3h3.173a20.847 20.847 0 0 1-.123-5Zm8.303 0H8.603a18.966 18.966 0 0 0 .135 5h6.524a18.974 18.974 0 0 0 .135-5Zm4.868 0h-3.358c.062.647.095 1.317.095 2a20.3 20.3 0 0 1-.218 3h3.173a8.482 8.482 0 0 0 .544-3c0-.689-.082-1.36-.236-2ZM8.88 4.09l-.023.008A8.531 8.531 0 0 0 4.25 8.5h3.048c.314-1.752.86-3.278 1.583-4.41ZM12 3.499l-.116.005C10.62 3.62 9.396 5.622 8.83 8.5h6.342c-.566-2.87-1.783-4.869-3.045-4.995L12 3.5Zm3.12.59.107.175c.669 1.112 1.177 2.572 1.475 4.237h3.048a8.533 8.533 0 0 0-4.339-4.29l-.291-.121Z", "link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z", "more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z", "open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z", diff --git a/app/javascript/shared/helpers/Validators.js b/app/javascript/shared/helpers/Validators.js index 3a53af9da..d8cf0b352 100644 --- a/app/javascript/shared/helpers/Validators.js +++ b/app/javascript/shared/helpers/Validators.js @@ -1,11 +1,22 @@ export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/); + export const isPhoneNumberValid = (value, dialCode) => { const number = value.replace(dialCode, ''); return !!number.match(/^[0-9]{1,14}$/); }; + export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === ''; + +export const isPhoneNumberValidWithDialCode = value => { + const number = value.replace(/^\+/, ''); // Remove the '+' sign + return !!number.match(/^[1-9]\d{4,}$/); // Validate the phone number with minimum 5 digits +}; + +export const startsWithPlus = value => value.startsWith('+'); + export const shouldBeUrl = (value = '') => value ? value.startsWith('http') : true; + export const isValidPassword = value => { const containsUppercase = /[A-Z]/.test(value); const containsLowercase = /[a-z]/.test(value); @@ -20,7 +31,9 @@ export const isValidPassword = value => { containsSpecialCharacter ); }; + export const isNumber = value => /^\d+$/.test(value); + export const isDomain = value => { if (value !== '') { const domainRegex = /^([\p{L}0-9]+(-[\p{L}0-9]+)*\.)+[a-z]{2,}$/gmu; diff --git a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js index 686ec5e4e..6d65cb36e 100644 --- a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js +++ b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js @@ -1,12 +1,56 @@ -import { shouldBeUrl } from '../Validators'; -import { isValidPassword } from '../Validators'; -import { isNumber } from '../Validators'; -import { isDomain } from '../Validators'; +import { + shouldBeUrl, + isPhoneNumberValidWithDialCode, + isPhoneE164OrEmpty, + isPhoneE164, + startsWithPlus, + isValidPassword, + isPhoneNumberValid, + isNumber, + isDomain, +} from '../Validators'; describe('#shouldBeUrl', () => { it('should return correct url', () => { expect(shouldBeUrl('http')).toEqual(true); }); + it('should return wrong url', () => { + expect(shouldBeUrl('')).toEqual(true); + expect(shouldBeUrl('abc')).toEqual(false); + }); +}); + +describe('#isPhoneE164', () => { + it('should return correct phone number', () => { + expect(isPhoneE164('+1234567890')).toEqual(true); + }); + it('should return wrong phone number', () => { + expect(isPhoneE164('1234567890')).toEqual(false); + expect(isPhoneE164('12345678A9')).toEqual(false); + expect(isPhoneE164('+12345678901234567890')).toEqual(false); + }); +}); + +describe('#isPhoneE164OrEmpty', () => { + it('should return correct phone number', () => { + expect(isPhoneE164OrEmpty('+1234567890')).toEqual(true); + expect(isPhoneE164OrEmpty('')).toEqual(true); + }); + it('should return wrong phone number', () => { + expect(isPhoneE164OrEmpty('1234567890')).toEqual(false); + expect(isPhoneE164OrEmpty('12345678A9')).toEqual(false); + expect(isPhoneE164OrEmpty('+12345678901234567890')).toEqual(false); + }); +}); + +describe('#isPhoneNumberValid', () => { + it('should return correct phone number', () => { + expect(isPhoneNumberValid('1234567890', '+91')).toEqual(true); + }); + it('should return wrong phone number', () => { + expect(isPhoneNumberValid('12345A67890', '+1')).toEqual(false); + expect(isPhoneNumberValid('12345A6789120', '+1')).toEqual(false); + }); }); describe('#isValidPassword', () => { @@ -51,3 +95,23 @@ describe('#isDomain', () => { expect(isDomain('https://test.in')).toEqual(false); }); }); + +describe('#isPhoneNumberValidWithDialCode', () => { + it('should return correct phone number', () => { + expect(isPhoneNumberValidWithDialCode('+123456789')).toEqual(true); + expect(isPhoneNumberValidWithDialCode('+12345')).toEqual(true); + }); + it('should return wrong phone number', () => { + expect(isPhoneNumberValidWithDialCode('+123')).toEqual(false); + expect(isPhoneNumberValidWithDialCode('+1234')).toEqual(false); + }); +}); + +describe('#startsWithPlus', () => { + it('should return correct phone number', () => { + expect(startsWithPlus('+123456789')).toEqual(true); + }); + it('should return wrong phone number', () => { + expect(startsWithPlus('123456789')).toEqual(false); + }); +}); diff --git a/app/javascript/widget/components/Form/PhoneInput.vue b/app/javascript/widget/components/Form/PhoneInput.vue new file mode 100644 index 000000000..755d64eda --- /dev/null +++ b/app/javascript/widget/components/Form/PhoneInput.vue @@ -0,0 +1,310 @@ + + + + diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 86211f589..12008e09f 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -22,10 +22,14 @@ :label-class="context => labelClass(context)" :input-class="context => inputClass(context)" :validation-messages="{ - isPhoneE164OrEmpty: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'), + startsWithPlus: $t( + 'PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DIAL_CODE_VALID_ERROR' + ), + isValidPhoneNumber: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'), email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'), required: $t('PRE_CHAT_FORM.REQUIRED'), }" + :has-error-in-phone-input="hasErrorInPhoneInput" /> field.enabled) .map(field => ({ ...field, - type: this.findFieldType(field.type), + type: + field.name === 'phoneNumber' + ? 'phoneInput' + : this.findFieldType(field.type), })); }, conversationCustomAttributes() { @@ -202,6 +211,9 @@ export default { if (classification === 'box' && type === 'checkbox') { return ''; } + if (type === 'phoneInput') { + this.hasErrorInPhoneInput = hasErrors; + } if (!hasErrors) { return `${this.inputStyles} hover:border-black-300 focus:border-black-300 ${this.isInputDarkOrLightMode} ${this.inputBorderColor}`; } @@ -224,12 +236,9 @@ export default { return this.formValues[name] || null; }, getValidation({ type, name }) { - if (!this.isContactFieldRequired(name)) { - return ''; - } const validations = { emailAddress: 'email', - phoneNumber: 'isPhoneE164OrEmpty', + phoneNumber: 'startsWithPlus|isValidPhoneNumber', url: 'url', date: 'date', text: null, @@ -238,11 +247,17 @@ export default { checkbox: false, }; const validationKeys = Object.keys(validations); - const validation = 'bail|required'; + const isRequired = this.isContactFieldRequired(name); + const validation = isRequired ? 'bail|required' : 'bail|optional'; + if (validationKeys.includes(name) || validationKeys.includes(type)) { const validationType = validations[type] || validations[name]; - return validationType ? `${validation}|${validationType}` : validation; + const validationString = validationType + ? `${validation}|${validationType}` + : validation; + return validationString; } + return ''; }, findFieldType(type) { diff --git a/app/javascript/widget/i18n/locale/en.json b/app/javascript/widget/i18n/locale/en.json index de94b33a3..67843ddda 100644 --- a/app/javascript/widget/i18n/locale/en.json +++ b/app/javascript/widget/i18n/locale/en.json @@ -66,7 +66,9 @@ "LABEL": "Phone Number", "PLACEHOLDER": "Please enter your phone number", "REQUIRED_ERROR": "Phone Number is required", - "VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555" + "DIAL_CODE_VALID_ERROR": "Please select a country code", + "VALID_ERROR": "Please enter a valid phone number", + "DROPDOWN_EMPTY": "No results found" }, "MESSAGE": { "LABEL": "Message",