<template> <div> <div class="flex flex-col" v-if="Object.keys(cards).length"> <div class="gap-y-2"> <div class="py-2 border-b hover:bg-gray-100 cursor-pointer" v-for="(name, key, id) in cards" :key="key" @click="onRegisterCard(id)" > <label> <input type="radio" :disabled="loading" :checked="register_card == id" > <span class="ltr:ml-2 rtl:mr-2">{{ name }}</span> </label> </div> </div> <div class="py-2 border-b hover:bg-gray-100 cursor-pointer" @click="onAddNewCard()"> <label> <input type="radio" id="card-new-card" name="new-card" :disabled="loading" :checked="new_card" > <span class="ltr:ml-2 rtl:mr-2">{{ textNewCard }}</span> </label> </div> <div class="flex justify-end" v-for="(name, key, id) in cards" :key="key"> <button v-if="register_card == id" type="button" :id="'card-'+ key + '-' + id" @click="onSelectedCard(key)" class="relative flex items-center justify-center px-6 py-1.5 my-2 bg-green hover:bg-green-700 text-white rounded-lg" :disabled="loading" > <i v-if="loading || register_card_loading" class="animate-submit delay-[0.28s] absolute w-2 h-2 rounded-full left-0 right-0 -top-3.5 m-auto before:absolute before:w-2 before:h-2 before:rounded-full before:animate-submit before:delay-[0.14s] after:absolute after:w-2 after:h-2 after:rounded-full after:animate-submit before:-left-3.5 after:-right-3.5 after:delay-[0.42s]" > </i> <span :class="[{'opacity-0': loading || register_card_loading}]"> {{ textButton }} </span> </button> </div> <div v-if="new_card" class="w-full mt-3" id="collapseNewCard"> <Card :fields="fields" :labels="formData" :isCardNumberMasked="isCardNumberMasked" :randomBackgrounds="randomBackgrounds" :backgroundImage="backgroundImage" /> <div class="grid sm:grid-cols-8 gap-x-4 gap-y-6 my-3.5"> <div class="sm:col-span-8"> <label for="cardName" class="form-control-label text-black text-sm font-medium"> {{ textCardName }} </label> <input type="text" :id="fields.cardName" v-letter-only @input="changeName" class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :placeholder="placeholderCardName" :value="formData.cardName" data-card-field autocomplete="off" /> <div class="text-red text-sm mt-1 block" v-if="validations.card_name" v-html="validations.card_name[0]"></div> </div> <div class="sm:col-span-8"> <label for="cardNumber" class="form-control-label text-black text-sm font-medium"> {{ textCardNumber }} </label> <input type="tel" :id="fields.cardNumber" @input="changeNumber" @focus="focusCardNumber" @blur="blurCardNumber" class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :placeholder="placeholderCardNumber" :value="formData.cardNumber" :maxlength="cardNumberMaxLength" data-card-field autocomplete="off" /> <div class="text-red text-sm mt-1 block" v-if="validations.card_number" v-html="validations.card_number[0]"></div> </div> <div class="sm:col-span-3"> <label for="cardMonth" class="form-control-label text-black text-sm font-medium"> {{ textExpirationDate }} </label> <select class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :id="fields.cardMonth" v-model="formData.cardMonth" @change="changeMonth" data-card-field > <option value disabled selected>{{ textMonth }}</option> <option v-bind:value="n < 10 ? '0' + n : n" v-for="n in 12" v-bind:disabled="n < minCardMonth" v-bind:key="n" >{{generateMonthValue(n)}}</option> </select> </div> <div class="sm:col-span-3 flex items-end"> <select class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :id="fields.cardYear" v-model="formData.cardYear" @change="changeYear" data-card-field > <option value="" disabled>{{ textYear }}</option> <option v-bind:value="$index + minCardYear" v-for="(n, $index) in 12" v-bind:key="n" >{{$index + minCardYear}}</option> </select> </div> <div class="sm:col-span-2"> <label for="cardCvv" class="form-control-label text-black text-sm font-medium"> {{ textCvv }} </label> <input type="tel" class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :placeholder="placeholderCvv" v-number-only :id="fields.cardCvv" maxlength="4" :value="formData.cardCvv" @input="changeCvv" data-card-field autocomplete="off" /> <div class="text-red text-sm mt-1 block" v-if="validations.card_cvv" v-html="validations.card_cvv[0]"></div> </div> <div class="sm:col-span-8 flex" :class="storeCard ? 'justify-between' : 'justify-end'"> <div class="flex items-center" v-if="storeCard"> <input @input="changeStoreCard" :id="'store_card' + _uid" name="store_card" type="checkbox" value="true" class="rounded-sm text-purple border-gray-300 cursor-pointer disabled:bg-gray-200 focus:outline-none focus:ring-transparent"> <label :for="'store_card' + _uid" class="form-control-label ltr:ml-2 rtl:ml-2"> <strong>{{ textStoreCard }}</strong> </label> </div> <button class="relative flex items-center justify-center px-6 py-1.5 bg-green hover:bg-green-700 text-white rounded-lg" v-on:click="invaildCard" :disabled="loading"> <i v-if="loading" class="animate-submit delay-[0.28s] absolute w-2 h-2 rounded-full left-0 right-0 -top-3.5 m-auto before:absolute before:w-2 before:h-2 before:rounded-full before:animate-submit before:delay-[0.14s] after:absolute after:w-2 after:h-2 after:rounded-full after:animate-submit before:-left-3.5 after:-right-3.5 after:delay-[0.42s]" > </i> <span :class="[{'opacity-0': loading}]"> {{ textButton }} </span> </button> </div> </div> </div> </div> <div class="flex flex-col" v-if="! Object.keys(cards).length"> <Card :fields="fields" :labels="formData" :isCardNumberMasked="isCardNumberMasked" :randomBackgrounds="randomBackgrounds" :backgroundImage="backgroundImage" /> <div class="grid sm:grid-cols-8 gap-x-4 gap-y-6 my-3.5"> <div class="sm:col-span-8"> <label for="cardName" class="form-control-label text-black text-sm font-medium"> {{ textCardName }} </label> <input type="text" :id="fields.cardName" v-letter-only @input="changeName" class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :placeholder="placeholderCardName" :value="formData.cardName" data-card-field autocomplete="off" /> <div class="text-red text-sm mt-1 block" v-if="validations.card_name" v-html="validations.card_name[0]"></div> </div> <div class="sm:col-span-8"> <label for="cardNumber" class="form-control-label text-black text-sm font-medium"> {{ textCardNumber }} </label> <input type="tel" :id="fields.cardNumber" @input="changeNumber" @focus="focusCardNumber" @blur="blurCardNumber" class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :placeholder="placeholderCardNumber" :value="formData.cardNumber" :maxlength="cardNumberMaxLength" data-card-field autocomplete="off" /> <div class="text-red text-sm mt-1 block" v-if="validations.card_number" v-html="validations.card_number[0]"></div> </div> <div class="sm:col-span-3"> <label for="cardMonth" class="form-control-label text-black text-sm font-medium"> {{ textExpirationDate }} </label> <div> <select class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :id="fields.cardMonth" v-model="formData.cardMonth" @change="changeMonth" data-card-field > <option value="" disabled>{{ textMonth }}</option> <option v-bind:value="n < 10 ? '0' + n : n" v-for="n in 12" v-bind:disabled="n < minCardMonth" v-bind:key="n" >{{ generateMonthValue(n) }}</option> </select> </div> </div> <div class="sm:col-span-3 flex items-end"> <select class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :id="fields.cardYear" v-model="formData.cardYear" @change="changeYear" data-card-field > <option value="" disabled>{{ textYear }}</option> <option v-bind:value="$index + minCardYear" v-for="(n, $index) in 12" v-bind:key="n" >{{ $index + minCardYear }}</option> </select> </div> <div class="sm:col-span-2"> <label for="cardCvv" class="form-control-label text-black text-sm font-medium"> {{ textCvv }} </label> <input type="tel" class="w-full text-sm px-3 py-2.5 mt-1 rounded-lg border border-light-gray text-black placeholder-light-gray bg-white disabled:bg-gray-200 focus:outline-none focus:ring-transparent focus:border-purple" :placeholder="placeholderCvv" v-number-only :id="fields.cardCvv" maxlength="4" :value="formData.cardCvv" @input="changeCvv" data-card-field autocomplete="off" /> <div class="text-red text-sm mt-1 block" v-if="validations.card_cvv" v-html="validations.card_cvv[0]"></div> </div> <div class="sm:col-span-8 flex" :class="storeCard ? 'justify-between' : 'justify-end'"> <div class="flex items-center" v-if="storeCard"> <input @input="changeStoreCard" :id="'store_card' + _uid" name="store_card" type="checkbox" value="true" class="rounded-sm text-purple border-gray-300 cursor-pointer disabled:bg-gray-200 focus:outline-none focus:ring-transparent"> <label :for="'store_card' + _uid" class="form-control-label ltr:ml-2 rtl:ml-2"> <strong>{{ textStoreCard }}</strong> </label> </div> <button v-if="! hideButton" class="relative flex items-center justify-center px-6 py-1.5 bg-green hover:bg-green-700 text-white rounded-lg" v-on:click="invaildCard" :disabled="loading"> <i v-if="loading" class="animate-submit delay-[0.28s] absolute w-2 h-2 rounded-full left-0 right-0 -top-3.5 m-auto before:absolute before:w-2 before:h-2 before:rounded-full before:animate-submit before:delay-[0.14s] after:absolute after:w-2 after:h-2 after:rounded-full after:animate-submit before:-left-3.5 after:-right-3.5 after:delay-[0.42s]" > </i> <span :class="[{'opacity-0': loading}]"> {{ textButton }} </span> </button> </div> </div> </div> </div> </template> <script> import axios from 'axios'; import Card from './Card'; export default { name: 'CardForm', directives: { 'number-only': { bind (el) { function checkValue (event) { event.target.value = event.target.value.replace(/[^0-9]/g, ''); if (event.charCode >= 48 && event.charCode <= 57) { return true; } event.preventDefault() } el.addEventListener('keypress', checkValue); } }, 'letter-only': { bind (el) { function checkValue (event) { if (event.charCode >= 48 && event.charCode <= 57) { event.preventDefault(); } return true; } el.addEventListener('keypress', checkValue) } } }, props: { cards: { type: [Array, Object], default: function () { return []; }, description: "Add Card Style" }, storeCard: { type: Boolean, default: false, icon: '', description: "Save card" }, path: { type: String, default: null, description: "Card Form Post Path" }, textCardNumber: { type: String, default: 'Card Number', description: "Label Card Number Text" }, placeholderCardNumber: { type: String, default: 'Enter Number', description: "Placeholder Card Number Text" }, textCard: { type: String, default: 'Cards', description: "Label Card Text" }, textNewCard: { type: String, default: 'New Card', description: "Label New Card Name Text" }, textCardName: { type: String, default: 'Card Name', description: "Label Card Name Text" }, placeholderCardName: { type: String, default: 'Enter Name', description: "Placeholder Card Name Text" }, textExpirationDate: { type: String, default: 'Expiration Date', description: "Label Expiration Date Text" }, textMonth: { type: String, default: 'Month', description: "Label Month Text" }, textYear: { type: String, default: 'Year', description: "Label Year Text" }, textCvv: { type: String, default: 'CVV', description: "Label CVV Text" }, placeholderCvv: { type: String, default: 'Enter CVV', description: "Placeholder Card CVV Text" }, hideButton: { type: Boolean, default: false, description: "Confirm button" }, textButton: { type: String, default: 'Confirm', description: "Add Card Style" }, textStoreCard: { type: String, default: 'Store Card', description: "Selected store card" }, formData: { type: Object, default: () => { return { cardName: '', cardNumber: '', cardMonth: '', cardYear: '', cardCvv: '', storeCard: false, card_id: 0, } } }, backgroundImage: [String, Object], randomBackgrounds: { type: Boolean, default: true } }, components: { Card }, data() { return { loading: false, validations:{}, fields: { cardNumber: 'v-card-number', cardName: 'v-card-name', cardMonth: 'v-card-month', cardYear: 'v-card-year', cardCvv: 'v-card-cvv', storeCard: 'v-card-store-card' }, minCardYear: new Date().getFullYear(), isCardNumberMasked: true, mainCardNumber: this.cardNumber, cardNumberMaxLength: 19, card_id: 0, new_card: false, register_card: 0, register_card_loading: false } }, computed: { minCardMonth() { if (this.formData.cardYear === this.minCardYear) { return new Date().getMonth() + 1; } return 1; } }, watch: { cardYear() { if (this.formData.cardMonth < this.minCardMonth) { this.formData.cardMonth = ''; } } }, mounted() { this.maskCardNumber(); }, methods: { onRegisterCard(id) { this.register_card = id; this.new_card = false; this.register_card_loading = true; setTimeout(() => { this.register_card_loading = false; }, 800); }, onAddNewCard() { this.new_card = true; this.register_card = null; }, onSelectedCard(card_id) { this.card_id = card_id; this.formData.card_id = card_id; this.invaildCard(); }, changeStoreCard(e) { this.formData.storeCard = false; if (e.target.checked) { this.formData.storeCard = true; } }, generateMonthValue(n) { return n < 10 ? `0${n}` : n; }, changeName(e) { this.formData.cardName = e.target.value; this.$emit('input-card-name', this.formData.cardName); }, changeNumber(e) { this.formData.cardNumber = e.target.value; let value = this.formData.cardNumber.replace(/\D/g, ''); // american express, 15 digits if ((/^3[47]\d{0,13}$/).test(value)) { this.formData.cardNumber = value.replace(/(\d{4})/, '$1 ').replace(/(\d{4}) (\d{6})/, '$1 $2 '); this.cardNumberMaxLength = 17; } else if ((/^3(?:0[0-5]|[68]\d)\d{0,11}$/).test(value)) { // diner's club, 14 digits this.formData.cardNumber = value.replace(/(\d{4})/, '$1 ').replace(/(\d{4}) (\d{6})/, '$1 $2 '); this.cardNumberMaxLength = 16; } else if ((/^\d{0,16}$/).test(value)) { // regular cc number, 16 digits this.formData.cardNumber = value.replace(/(\d{4})/, '$1 ').replace(/(\d{4}) (\d{4})/, '$1 $2 ').replace(/(\d{4}) (\d{4}) (\d{4})/, '$1 $2 $3 '); this.cardNumberMaxLength = 19; } this.$emit('input-card-number', this.formData.cardNumber); }, changeMonth() { this.$emit('input-card-month', this.formData.cardMonth); }, changeYear() { this.$emit('input-card-year', this.formData.cardYear); }, changeCvv(e) { this.formData.cardCvv = e.target.value; this.$emit('input-card-cvv', this.formData.cardCvv); }, invaildCard() { this.loading = true; let number = this.formData.cardNumber; let sum = 0; let isOdd = true; this.unMaskCardNumber(); /* for (let i = number.length - 1; i >= 0; i--) { let num = number.charAt(i); if (isOdd) { sum += num; } else { num = num * 2; if (num > 9) { num = num.toString().split('').join('+'); } sum += num; } isOdd = !isOdd; } */ this.formData.card_id = this.card_id; if (sum % 10 !== 0) { alert('invaild card number'); this.loading = false; } else { axios.post(this.path, this.formData) .then(response => { this.loading = false; if (response.data.redirect) { this.loading = true; window.location.href = response.data.redirect; } }) .catch(error => { this.loading = false; this.validations = error.response.data.errors; }); } }, blurCardNumber() { if (this.isCardNumberMasked) { this.maskCardNumber(); } }, maskCardNumber() { this.mainCardNumber = this.formData.cardNumber; let arr = this.formData.cardNumber.split(''); arr.forEach((element, index) => { if (index > 4 && index < 14 && element.trim() !== '') { arr[index] = '*'; } }); this.formData.cardNumber = arr.join(''); }, unMaskCardNumber() { this.formData.cardNumber = this.mainCardNumber; }, focusCardNumber() { this.unMaskCardNumber(); }, toggleMask() { this.isCardNumberMasked = !this.isCardNumberMasked; if (this.isCardNumberMasked) { this.maskCardNumber(); } else { this.unMaskCardNumber(); } }, showNewCard() { } } } </script>