|
@@ -42,7 +42,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
<q-input
|
|
<q-input
|
|
|
v-model="form.card_number"
|
|
v-model="form.card_number"
|
|
|
- :rules="[inputRules.requiredHideMessage]"
|
|
|
|
|
|
|
+ :rules="cardNumberRules"
|
|
|
:error="!!serverErrors.card_number"
|
|
:error="!!serverErrors.card_number"
|
|
|
:error-message="serverErrors.card_number"
|
|
:error-message="serverErrors.card_number"
|
|
|
mask="#### #### #### ####"
|
|
mask="#### #### #### ####"
|
|
@@ -102,7 +102,7 @@
|
|
|
<q-input
|
|
<q-input
|
|
|
v-model="form.cvv"
|
|
v-model="form.cvv"
|
|
|
placeholder="***"
|
|
placeholder="***"
|
|
|
- :rules="[inputRules.requiredHideMessage]"
|
|
|
|
|
|
|
+ :rules="cvvRules"
|
|
|
:error="!!serverErrors.cvv"
|
|
:error="!!serverErrors.cvv"
|
|
|
:error-message="serverErrors.cvv"
|
|
:error-message="serverErrors.cvv"
|
|
|
mask="####"
|
|
mask="####"
|
|
@@ -131,7 +131,7 @@
|
|
|
class="full-width q-mt-md save-btn q-mb-md"
|
|
class="full-width q-mt-md save-btn q-mb-md"
|
|
|
padding="14px 16px"
|
|
padding="14px 16px"
|
|
|
:label="paymentMethod ? $t('profile.payments.save_btn') : $t('profile.payments.add_card')"
|
|
:label="paymentMethod ? $t('profile.payments.save_btn') : $t('profile.payments.add_card')"
|
|
|
- :loading="loading"
|
|
|
|
|
|
|
+ :loading="loading || tokenizing"
|
|
|
:disable="!hasUpdatedFields"
|
|
:disable="!hasUpdatedFields"
|
|
|
/>
|
|
/>
|
|
|
</q-form>
|
|
</q-form>
|
|
@@ -143,7 +143,7 @@
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, computed } from 'vue';
|
|
import { ref, computed } from 'vue';
|
|
|
-import { useDialogPluginComponent } from 'quasar';
|
|
|
|
|
|
|
+import { useDialogPluginComponent, useQuasar } from 'quasar';
|
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
|
|
import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
|
|
|
import { useSubmitHandler } from 'src/composables/useSubmitHandler';
|
|
import { useSubmitHandler } from 'src/composables/useSubmitHandler';
|
|
@@ -151,6 +151,37 @@ import { useInputRules } from 'src/composables/useInputRules';
|
|
|
import { /*validateCardNumberLuhn,*/ detectCardBrand, validateCardExpiration } from 'src/helpers/utils';
|
|
import { /*validateCardNumberLuhn,*/ detectCardBrand, validateCardExpiration } from 'src/helpers/utils';
|
|
|
import { createClientPaymentMethod, updateClientPaymentMethod } from 'src/api/clientPaymentMethod';
|
|
import { createClientPaymentMethod, updateClientPaymentMethod } from 'src/api/clientPaymentMethod';
|
|
|
|
|
|
|
|
|
|
+const PAGARME_SCRIPT_URL = 'https://assets.pagar.me/pagarme-js/4.5/pagarme.min.js';
|
|
|
|
|
+let pagarmeScriptPromise = null;
|
|
|
|
|
+
|
|
|
|
|
+const loadPagarmeScript = () => {
|
|
|
|
|
+ if (window.pagarme) {
|
|
|
|
|
+ return Promise.resolve(window.pagarme);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!pagarmeScriptPromise) {
|
|
|
|
|
+ pagarmeScriptPromise = new Promise((resolve, reject) => {
|
|
|
|
|
+ const existingScript = document.querySelector('script[data-pagarme-js="true"]');
|
|
|
|
|
+
|
|
|
|
|
+ if (existingScript) {
|
|
|
|
|
+ existingScript.addEventListener('load', () => resolve(window.pagarme), { once: true });
|
|
|
|
|
+ existingScript.addEventListener('error', () => reject(new Error('Falha ao carregar o Pagar.me JS')), { once: true });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const script = document.createElement('script');
|
|
|
|
|
+ script.src = PAGARME_SCRIPT_URL;
|
|
|
|
|
+ script.async = true;
|
|
|
|
|
+ script.setAttribute('data-pagarme-js', 'true');
|
|
|
|
|
+ script.onload = () => resolve(window.pagarme);
|
|
|
|
|
+ script.onerror = () => reject(new Error('Falha ao carregar o Pagar.me JS'));
|
|
|
|
|
+ document.head.appendChild(script);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return pagarmeScriptPromise;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
paymentMethod: {
|
|
paymentMethod: {
|
|
|
type: Object,
|
|
type: Object,
|
|
@@ -165,16 +196,18 @@ const props = defineProps({
|
|
|
defineEmits([...useDialogPluginComponent.emits]);
|
|
defineEmits([...useDialogPluginComponent.emits]);
|
|
|
|
|
|
|
|
const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
|
|
|
+const $q = useQuasar();
|
|
|
const { t } = useI18n();
|
|
const { t } = useI18n();
|
|
|
const { inputRules } = useInputRules();
|
|
const { inputRules } = useInputRules();
|
|
|
const formRef = ref(null);
|
|
const formRef = ref(null);
|
|
|
|
|
+const tokenizing = ref(false);
|
|
|
|
|
|
|
|
const { form, hasUpdatedFields } = useFormUpdateTracker({
|
|
const { form, hasUpdatedFields } = useFormUpdateTracker({
|
|
|
client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
|
|
client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
|
|
|
- card_number: props.paymentMethod ? props.paymentMethod.card_number : null,
|
|
|
|
|
|
|
+ card_number: null,
|
|
|
holder_name: props.paymentMethod ? props.paymentMethod.holder_name : null,
|
|
holder_name: props.paymentMethod ? props.paymentMethod.holder_name : null,
|
|
|
expiration: props.paymentMethod ? (props.paymentMethod.expiration?.includes('/') && props.paymentMethod.expiration.split('/')[1].length === 2 ? `${props.paymentMethod.expiration.split('/')[0]}/20${props.paymentMethod.expiration.split('/')[1]}` : props.paymentMethod.expiration) : null,
|
|
expiration: props.paymentMethod ? (props.paymentMethod.expiration?.includes('/') && props.paymentMethod.expiration.split('/')[1].length === 2 ? `${props.paymentMethod.expiration.split('/')[0]}/20${props.paymentMethod.expiration.split('/')[1]}` : props.paymentMethod.expiration) : null,
|
|
|
- cvv: props.paymentMethod ? props.paymentMethod.cvv : null,
|
|
|
|
|
|
|
+ cvv: null,
|
|
|
card_name: props.paymentMethod ? props.paymentMethod.card_name : null,
|
|
card_name: props.paymentMethod ? props.paymentMethod.card_name : null,
|
|
|
brand: props.paymentMethod ? props.paymentMethod.brand : null,
|
|
brand: props.paymentMethod ? props.paymentMethod.brand : null,
|
|
|
last_four_digits: props.paymentMethod ? props.paymentMethod.last_four_digits : null,
|
|
last_four_digits: props.paymentMethod ? props.paymentMethod.last_four_digits : null,
|
|
@@ -183,6 +216,14 @@ const { form, hasUpdatedFields } = useFormUpdateTracker({
|
|
|
|
|
|
|
|
const { loading, serverErrors, execute: submitForm } = useSubmitHandler(() => onDialogOK(true));
|
|
const { loading, serverErrors, execute: submitForm } = useSubmitHandler(() => onDialogOK(true));
|
|
|
|
|
|
|
|
|
|
+const cardNumberRules = computed(() => (
|
|
|
|
|
+ props.paymentMethod ? [] : [inputRules.requiredHideMessage]
|
|
|
|
|
+));
|
|
|
|
|
+
|
|
|
|
|
+const cvvRules = computed(() => (
|
|
|
|
|
+ props.paymentMethod ? [] : [inputRules.requiredHideMessage]
|
|
|
|
|
+));
|
|
|
|
|
+
|
|
|
const brandDisplayName = computed(() => {
|
|
const brandDisplayName = computed(() => {
|
|
|
if (!form.brand) return '';
|
|
if (!form.brand) return '';
|
|
|
const names = {
|
|
const names = {
|
|
@@ -198,9 +239,16 @@ const brandDisplayName = computed(() => {
|
|
|
|
|
|
|
|
const maskedCardNumberPreview = computed(() => {
|
|
const maskedCardNumberPreview = computed(() => {
|
|
|
const raw = form.card_number ? String(form.card_number).replace(/\D/g, '') : '';
|
|
const raw = form.card_number ? String(form.card_number).replace(/\D/g, '') : '';
|
|
|
- if (!raw || raw.includes('*')) return '**** **** **** ****';
|
|
|
|
|
- const lastFour = raw.slice(-4).padStart(4, '*');
|
|
|
|
|
- return `**** **** **** ${lastFour}`;
|
|
|
|
|
|
|
+ if (raw && !raw.includes('*')) {
|
|
|
|
|
+ const lastFour = raw.slice(-4).padStart(4, '*');
|
|
|
|
|
+ return `**** **** **** ${lastFour}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (form.last_four_digits) {
|
|
|
|
|
+ return `**** **** **** ${String(form.last_four_digits).padStart(4, '*')}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return '**** **** **** ****';
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// const validateCardNumber = (val) => {
|
|
// const validateCardNumber = (val) => {
|
|
@@ -219,6 +267,12 @@ const validateExpiration = (val) => {
|
|
|
const onCardNumberChange = () => {
|
|
const onCardNumberChange = () => {
|
|
|
serverErrors.value.card_number = null;
|
|
serverErrors.value.card_number = null;
|
|
|
const raw = form.card_number ? String(form.card_number).replace(/\D/g, '') : '';
|
|
const raw = form.card_number ? String(form.card_number).replace(/\D/g, '') : '';
|
|
|
|
|
+ if (!raw) {
|
|
|
|
|
+ form.brand = null;
|
|
|
|
|
+ form.last_four_digits = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (raw.length >= 6 && !String(form.card_number).includes('*')) {
|
|
if (raw.length >= 6 && !String(form.card_number).includes('*')) {
|
|
|
const brand = detectCardBrand(raw);
|
|
const brand = detectCardBrand(raw);
|
|
|
if (brand) form.brand = brand;
|
|
if (brand) form.brand = brand;
|
|
@@ -226,13 +280,88 @@ const onCardNumberChange = () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+const buildPayload = (extra = {}) => ({
|
|
|
|
|
+ client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
|
|
|
|
|
+ holder_name: form.holder_name,
|
|
|
|
|
+ expiration: form.expiration,
|
|
|
|
|
+ card_name: form.card_name,
|
|
|
|
|
+ brand: form.brand,
|
|
|
|
|
+ last_four_digits: form.last_four_digits,
|
|
|
|
|
+ is_active: form.is_active,
|
|
|
|
|
+ ...extra,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const tokenizeCard = async () => {
|
|
|
|
|
+ const encryptionKey = process.env.PAGARME_ENCRYPTION_KEY;
|
|
|
|
|
+
|
|
|
|
|
+ if (!encryptionKey) {
|
|
|
|
|
+ throw new Error('PAGARME_ENCRYPTION_KEY não configurada');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await loadPagarmeScript();
|
|
|
|
|
+
|
|
|
|
|
+ const rawCardNumber = form.card_number ? String(form.card_number).replace(/\D/g, '') : '';
|
|
|
|
|
+ const rawCvv = form.cvv ? String(form.cvv).replace(/\D/g, '') : '';
|
|
|
|
|
+
|
|
|
|
|
+ const card = {
|
|
|
|
|
+ card_holder_name: form.holder_name,
|
|
|
|
|
+ card_expiration_date: form.expiration,
|
|
|
|
|
+ card_number: rawCardNumber,
|
|
|
|
|
+ card_cvv: rawCvv,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const validations = window.pagarme?.validate?.({ card });
|
|
|
|
|
+ if (validations?.card) {
|
|
|
|
|
+ if (!validations.card.card_number) {
|
|
|
|
|
+ throw new Error(t('profile.payments.invalid_card_number'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!validations.card.card_holder_name) {
|
|
|
|
|
+ throw new Error(t('profile.payments.holder_name'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!validations.card.card_expiration_date) {
|
|
|
|
|
+ throw new Error(t('profile.payments.expired_card'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!validations.card.card_cvv) {
|
|
|
|
|
+ throw new Error(t('profile.payments.cvv'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const client = await window.pagarme.client.connect({ encryption_key: encryptionKey });
|
|
|
|
|
+ return client.security.encrypt(card);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const onOKClick = async () => {
|
|
const onOKClick = async () => {
|
|
|
const valid = await formRef.value?.validate();
|
|
const valid = await formRef.value?.validate();
|
|
|
if (!valid) return;
|
|
if (!valid) return;
|
|
|
|
|
+
|
|
|
if (props.paymentMethod) {
|
|
if (props.paymentMethod) {
|
|
|
- await submitForm(() => updateClientPaymentMethod(props.paymentMethod.id, { ...form }));
|
|
|
|
|
- } else {
|
|
|
|
|
- await submitForm(() => createClientPaymentMethod({ ...form }));
|
|
|
|
|
|
|
+ const payload = buildPayload();
|
|
|
|
|
+ await submitForm(() => updateClientPaymentMethod(props.paymentMethod.id, payload));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tokenizing.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const token = await tokenizeCard();
|
|
|
|
|
+ const payload = buildPayload({
|
|
|
|
|
+ token,
|
|
|
|
|
+ card_number: null,
|
|
|
|
|
+ cvv: null,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ await submitForm(() => createClientPaymentMethod(payload));
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Erro ao tokenizar o cartão:', error);
|
|
|
|
|
+ $q.notify({
|
|
|
|
|
+ type: 'negative',
|
|
|
|
|
+ message: error instanceof Error ? error.message : 'Não foi possível tokenizar o cartão.',
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ tokenizing.value = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
</script>
|
|
</script>
|