Sfoglia il codice sorgente

feat: add tokenizacao do cartao

Gustavo Mantovani 1 mese fa
parent
commit
253f343f18
2 ha cambiato i file con 142 aggiunte e 12 eliminazioni
  1. 1 0
      quasar.config.js
  2. 141 12
      src/components/profile/ProfilePaymentAddDialog.vue

+ 1 - 0
quasar.config.js

@@ -71,6 +71,7 @@ export default defineConfig((ctx) => {
         WEBSOCKET_API_KEY:
           "7wArC/kl0nTbt4zBu0agw.NXLyjA96I6x1XmBcuokwPqfo3/CIxzqYw.PTthh5eqa08Uf4ubFlOqatpShoz1CRRID9pZReEFvBk3il6E9u",
         GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY ?? "",
+        PAGARME_ENCRYPTION_KEY: process.env.PAGARME_ENCRYPTION_KEY ?? "", // nunca colocar a chave privada, aqui eh a chave publica
       },
       // rawDefine: {}
       // ignorePublicFolder: true,

+ 141 - 12
src/components/profile/ProfilePaymentAddDialog.vue

@@ -42,7 +42,7 @@
             </div>
             <q-input
               v-model="form.card_number"
-              :rules="[inputRules.requiredHideMessage]"
+              :rules="cardNumberRules"
               :error="!!serverErrors.card_number"
               :error-message="serverErrors.card_number"
               mask="#### #### #### ####"
@@ -102,7 +102,7 @@
               <q-input
                 v-model="form.cvv"
                 placeholder="***"
-                :rules="[inputRules.requiredHideMessage]"
+                :rules="cvvRules"
                 :error="!!serverErrors.cvv"
                 :error-message="serverErrors.cvv"
                 mask="####"
@@ -131,7 +131,7 @@
             class="full-width q-mt-md save-btn q-mb-md"
             padding="14px 16px"
             :label="paymentMethod ? $t('profile.payments.save_btn') : $t('profile.payments.add_card')"
-            :loading="loading"
+            :loading="loading || tokenizing"
             :disable="!hasUpdatedFields"
           />
         </q-form>
@@ -143,7 +143,7 @@
 
 <script setup>
 import { ref, computed } from 'vue';
-import { useDialogPluginComponent } from 'quasar';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
 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 { 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({
   paymentMethod: {
     type: Object,
@@ -165,16 +196,18 @@ const props = defineProps({
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const $q = useQuasar();
 const { t } = useI18n();
 const { inputRules } = useInputRules();
 const formRef = ref(null);
+const tokenizing = ref(false);
 
 const { form, hasUpdatedFields } = useFormUpdateTracker({
   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,
   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,
   brand: props.paymentMethod ? props.paymentMethod.brand : 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 cardNumberRules = computed(() => (
+  props.paymentMethod ? [] : [inputRules.requiredHideMessage]
+));
+
+const cvvRules = computed(() => (
+  props.paymentMethod ? [] : [inputRules.requiredHideMessage]
+));
+
 const brandDisplayName = computed(() => {
   if (!form.brand) return '';
   const names = {
@@ -198,9 +239,16 @@ const brandDisplayName = computed(() => {
 
 const maskedCardNumberPreview = computed(() => {
   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) => {
@@ -219,6 +267,12 @@ const validateExpiration = (val) => {
 const onCardNumberChange = () => {
   serverErrors.value.card_number = null;
   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('*')) {
     const brand = detectCardBrand(raw);
     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 valid = await formRef.value?.validate();
   if (!valid) return;
+
   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>