Explorar o código

refactor: tokenizacao do cartao

Gustavo Mantovani hai 1 mes
pai
achega
f9bd2ec6aa
Modificáronse 2 ficheiros con 406 adicións e 167 borrados
  1. 1 1
      quasar.config.js
  2. 405 166
      src/components/profile/ProfilePaymentAddDialog.vue

+ 1 - 1
quasar.config.js

@@ -71,7 +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
+        PAGARME_PUBLIC_KEY: process.env.PAGARME_PUBLIC_KEY ?? "pk_4eEdVPwf0msoGXkr",
       },
       // rawDefine: {}
       // ignorePublicFolder: true,

+ 405 - 166
src/components/profile/ProfilePaymentAddDialog.vue

@@ -1,14 +1,73 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    persistent
+    transition-hide="slide-down"
+    transition-show="slide-up"
+  >
     <div class="bg-page full-height column no-shadow">
+      <form
+        ref="pagarmeFormRef"
+        action="#"
+        class="pagarme-token-form"
+        data-pagarmecheckout-form
+        method="POST"
+      >
+        <input
+          v-model="form.holder_name"
+          data-pagarmecheckout-element="holder_name"
+          name="holder-name"
+          type="text"
+        >
+
+        <input
+          v-model="pagarmeCardNumber"
+          data-pagarmecheckout-element="number"
+          name="card-number"
+          type="text"
+        >
+
+        <input
+          v-model="pagarmeExpirationMonth"
+          data-pagarmecheckout-element="exp_month"
+          name="card-exp-month"
+          type="text"
+        >
+
+        <input
+          v-model="pagarmeExpirationYear"
+          data-pagarmecheckout-element="exp_year"
+          name="card-exp-year"
+          type="text"
+        >
+
+        <input
+          v-model="pagarmeCvv"
+          data-pagarmecheckout-element="cvv"
+          name="cvv"
+          type="text"
+        >
+      </form>
 
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+          @click="onDialogCancel"
+        />
+
         <q-space />
+
         <span class="text-subtitle1 text-weight-bold text-primary">
           {{ $t('profile.payments.title') }}
         </span>
+
         <q-space />
+
         <div style="width: 32px"></div>
       </div>
 
@@ -19,38 +78,59 @@
 
         <div class="virtual-card q-mb-xl">
           <div class="card-top-row row items-start justify-between">
-            <span class="card-brand-label">{{ brandDisplayName }}</span>
-            <q-icon name="mdi-wifi-strength-4" size="24px" color="white" class="nfc-icon" style="transform: rotate(90deg);" />
+            <span class="card-brand-label">
+              {{ brandDisplayName }}
+            </span>
+
+            <q-icon
+              class="nfc-icon"
+              color="white"
+              name="mdi-wifi-strength-4"
+              size="24px"
+              style="transform: rotate(90deg);"
+            />
           </div>
+
           <div class="card-number-preview">
             {{ maskedCardNumberPreview }}
           </div>
+
           <div class="card-bottom-row row items-end justify-between">
             <div class="column">
-              <span class="card-holder-name">{{ form.holder_name || 'Nome do Titular' }}</span>
+              <span class="card-holder-name">
+                {{ form.holder_name || 'Nome do Titular' }}
+              </span>
             </div>
+
             <div class="column items-end">
-              <span class="card-expiry">{{ form.expiration || '**/****' }}</span>
+              <span class="card-expiry">
+                {{ form.expiration || '**/****' }}
+              </span>
             </div>
           </div>
         </div>
 
-        <q-form ref="formRef" class="column q-gutter-y-sm text-text" @submit="onOKClick">
+        <q-form
+          ref="formRef"
+          class="column q-gutter-y-sm text-text"
+          @submit="onOKClick"
+        >
           <div>
             <div class="input-label">
               {{ $t('profile.payments.card_number') }}
             </div>
+
             <q-input
               v-model="form.card_number"
-              :rules="cardNumberRules"
-              :error="!!serverErrors.card_number"
-              :error-message="serverErrors.card_number"
+              class="input-field bg-surface input-border-dark"
+              hide-bottom-space
+              input-class="text-text"
               mask="#### #### #### ####"
-              unmasked-value
               outlined
-              input-class="text-text"
-              hide-bottom-space
-              class="input-field bg-surface input-border-dark"
+              unmasked-value
+              :error="!!serverErrors.card_number"
+              :error-message="serverErrors.card_number"
+              :rules="cardNumberRules"
               @update:model-value="onCardNumberChange"
             />
           </div>
@@ -59,16 +139,17 @@
             <div class="input-label">
               {{ $t('profile.payments.holder_name') }}
             </div>
+
             <q-input
               v-model="form.holder_name"
+              class="input-field bg-surface input-border-dark"
+              hide-bottom-space
+              input-class="text-text"
+              outlined
               placeholder="Nome como está no cartão"
-              :rules="[inputRules.requiredHideMessage]"
               :error="!!serverErrors.holder_name"
               :error-message="serverErrors.holder_name"
-              outlined
-              input-class="text-text"
-              hide-bottom-space
-              class="input-field bg-surface input-border-dark"
+              :rules="[inputRules.requiredHideMessage]"
               @update:model-value="serverErrors.holder_name = null"
             />
           </div>
@@ -78,169 +159,206 @@
               <div class="input-label">
                 {{ $t('profile.payments.expiration') }}
               </div>
+
               <q-input
                 v-model="form.expiration"
-                :placeholder="$t('profile.payments.mmyyyy')"
-                :rules="[inputRules.requiredHideMessage, validateExpiration]"
-                :error="!!serverErrors.expiration"
-                :error-message="serverErrors.expiration"
-                mask="##/####"
-                outlined
                 class="col input-field bg-surface input-border-dark"
-                input-class="text-text"
                 hide-bottom-space
+                input-class="text-text"
+                mask="##/####"
+                outlined
+                :error="!!serverErrors.expiration"
+                :error-message="serverErrors.expiration"
+                :placeholder="$t('profile.payments.mmyyyy')"
+                :rules="[inputRules.requiredHideMessage, validateExpiration]"
                 @update:model-value="serverErrors.expiration = null"
               />
             </div>
+
             <div class="col-5">
               <div class="input-label row items-center q-gutter-x-xs">
-                <span>{{ $t('profile.payments.cvv') }}</span>
-                <q-icon name="mdi-help-circle-outline" color="grey-8" size="16px" class="cursor-pointer">
-                  <q-tooltip>{{ $t('profile.payments.cvv_help') }}</q-tooltip>
+                <span>
+                  {{ $t('profile.payments.cvv') }}
+                </span>
+
+                <q-icon
+                  class="cursor-pointer"
+                  color="grey-8"
+                  name="mdi-help-circle-outline"
+                  size="16px"
+                >
+                  <q-tooltip>
+                    {{ $t('profile.payments.cvv_help') }}
+                  </q-tooltip>
                 </q-icon>
               </div>
+
               <q-input
                 v-model="form.cvv"
+                class="col input-field bg-surface input-border-dark"
+                hide-bottom-space
+                input-class="text-text"
+                mask="####"
+                outlined
                 placeholder="***"
-                :rules="cvvRules"
+                type="password"
+                unmasked-value
                 :error="!!serverErrors.cvv"
                 :error-message="serverErrors.cvv"
-                mask="####"
-                unmasked-value
-                type="password"
-                outlined
-                input-class="text-text"
-                class="col input-field bg-surface input-border-dark"
-                hide-bottom-space
+                :rules="cvvRules"
                 @update:model-value="serverErrors.cvv = null"
               />
             </div>
           </div>
 
           <div class="row items-center justify-center q-gutter-x-xs q-mt-sm q-mb-xs">
-            <q-icon name="mdi-shield-check" color="positive" size="18px" />
-            <span class="security-text-new">{{ $t('profile.payments.security_badge') }}</span>
+            <q-icon
+              color="positive"
+              name="mdi-shield-check"
+              size="18px"
+            />
+
+            <span class="security-text-new">
+              {{ $t('profile.payments.security_badge') }}
+            </span>
           </div>
 
           <q-btn
-            type="submit"
-            unelevated
-            rounded
-            no-caps
-            color="primary"
             class="full-width q-mt-md save-btn q-mb-md"
+            color="primary"
+            no-caps
             padding="14px 16px"
+            rounded
+            type="submit"
+            unelevated
+            :disable="!hasUpdatedFields"
             :label="paymentMethod ? $t('profile.payments.save_btn') : $t('profile.payments.add_card')"
             :loading="loading || tokenizing"
-            :disable="!hasUpdatedFields"
           />
         </q-form>
       </div>
-
     </div>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
+import { computed, ref } from 'vue';
+
 import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
-import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
-import { useSubmitHandler } from 'src/composables/useSubmitHandler';
-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;
-      }
+import { createClientPaymentMethod, updateClientPaymentMethod } from 'src/api/clientPaymentMethod';
 
-      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);
-    });
-  }
+import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
+import { useInputRules } from 'src/composables/useInputRules';
+import { useSubmitHandler } from 'src/composables/useSubmitHandler';
 
-  return pagarmeScriptPromise;
-};
+import { detectCardBrand, validateCardExpiration } from 'src/helpers/utils';
 
 const props = defineProps({
-  paymentMethod: {
-    type: Object,
-    default: null,
-  },
   clientId: {
-    type: Number,
     required: true,
+    type: Number,
+  },
+
+  paymentMethod: {
+    default: null,
+    type: Object,
   },
 });
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 const $q = useQuasar();
-const { t } = useI18n();
+
+const { dialogRef, onDialogCancel, onDialogOK } = useDialogPluginComponent();
+
 const { inputRules } = useInputRules();
-const formRef = ref(null);
-const tokenizing = ref(false);
+
+const { t } = useI18n();
 
 const { form, hasUpdatedFields } = useFormUpdateTracker({
-  client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
+  brand: props.paymentMethod ? props.paymentMethod.brand : null,
+  card_name: props.paymentMethod ? props.paymentMethod.card_name : 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,
+  client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
   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,
+
+  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,
+
+  holder_name: props.paymentMethod ? props.paymentMethod.holder_name : null,
   is_active: props.paymentMethod ? props.paymentMethod.is_active : true,
+  last_four_digits: props.paymentMethod ? props.paymentMethod.last_four_digits : null,
 });
 
-const { loading, serverErrors, execute: submitForm } = useSubmitHandler(() => onDialogOK(true));
+const {
+  execute: submitForm,
+  loading,
+  serverErrors,
+} = useSubmitHandler(() => onDialogOK(true));
 
-const cardNumberRules = computed(() => (
-  props.paymentMethod ? [] : [inputRules.requiredHideMessage]
-));
+const formRef = ref(null);
 
-const cvvRules = computed(() => (
-  props.paymentMethod ? [] : [inputRules.requiredHideMessage]
-));
+const pagarmeFormRef = ref(null);
+
+const tokenizing = ref(false);
 
 const brandDisplayName = computed(() => {
-  if (!form.brand) return '';
+  if (!form.brand) {
+    return '';
+  }
+
   const names = {
-    visa: 'VISA',
-    mastercard: 'Mastercard',
-    elo: 'Elo',
-    hipercard: 'Hipercard',
     diners: 'Diners',
     discover: 'Discover',
+    elo: 'Elo',
+    hipercard: 'Hipercard',
+    mastercard: 'Mastercard',
+    visa: 'VISA',
   };
+
   return names[form.brand] ?? form.brand;
 });
 
+const cardNumberRules = computed(() => (
+  props.paymentMethod
+    ? []
+    : [inputRules.requiredHideMessage]
+));
+
+const cvvRules = computed(() => (
+  props.paymentMethod
+    ? []
+    : [inputRules.requiredHideMessage]
+));
+
+const expirationMonth = computed(() => (
+  form.expiration
+    ? form.expiration.split('/')[0]
+    : ''
+));
+
+const expirationYear = computed(() => (
+  form.expiration
+    ? form.expiration.split('/')[1]
+    : ''
+));
+
 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('*')) {
     const lastFour = raw.slice(-4).padStart(4, '*');
+
     return `**** **** **** ${lastFour}`;
   }
 
@@ -251,95 +369,201 @@ const maskedCardNumberPreview = computed(() => {
   return '**** **** **** ****';
 });
 
-// const validateCardNumber = (val) => {
-//   if (!val) return true;
-//   if (val.includes('*')) return true;
-//   if (!validateCardNumberLuhn(val)) return t('profile.payments.invalid_card_number');
-//   return true;
-// };
+const pagarmeCardNumber = computed({
+  get: () => (
+    form.card_number
+      ? String(form.card_number).replace(/\D/g, '')
+      : ''
+  ),
+  set: () => {},
+});
 
-const validateExpiration = (val) => {
-  if (!val) return true;
-  if (!validateCardExpiration(val)) return t('profile.payments.expired_card');
-  return true;
+const pagarmeCvv = computed({
+  get: () => (
+    form.cvv
+      ? String(form.cvv).replace(/\D/g, '')
+      : ''
+  ),
+  set: () => {},
+});
+
+const pagarmeExpirationMonth = computed({
+  get: () => expirationMonth.value,
+  set: () => {},
+});
+
+const pagarmeExpirationYear = computed({
+  get: () => expirationYear.value,
+  set: () => {},
+});
+
+const PAGARME_SCRIPT_URL = 'https://checkout.pagar.me/v1/tokenizecard.js';
+
+let pagarmeScriptPromise = null;
+
+const getPagarmePublicKey = () => (
+  process.env.PAGARME_PUBLIC_KEY
+);
+
+const getPagarmeTokenFromResponse = (data) => {
+  const tokenKey = Object.keys(data || {}).find((key) => (
+    key.startsWith('pagarmetoken')
+  ));
+
+  return tokenKey ? data[tokenKey] : null;
+};
+
+const loadPagarmeScript = () => {
+  if (window.PagarmeCheckout) {
+    return Promise.resolve(window.PagarmeCheckout);
+  }
+
+  if (!pagarmeScriptPromise) {
+    pagarmeScriptPromise = new Promise((resolve, reject) => {
+      const publicKey = getPagarmePublicKey();
+
+      if (!publicKey) {
+        reject(new Error('PAGARME_PUBLIC_KEY não configurada'));
+        return;
+      }
+
+      const existingScript = document.querySelector('script[data-pagarmecheckout-script="true"]');
+
+      if (existingScript) {
+        existingScript.addEventListener(
+          'error',
+          () => reject(new Error('Falha ao carregar o tokenizecard.js')),
+          { once: true },
+        );
+
+        existingScript.addEventListener(
+          'load',
+          () => resolve(window.PagarmeCheckout),
+          { once: true },
+        );
+
+        return;
+      }
+
+      const script = document.createElement('script');
+
+      script.async = true;
+      script.onerror = () => reject(new Error('Falha ao carregar o tokenizecard.js'));
+      script.onload = () => resolve(window.PagarmeCheckout);
+      script.src = PAGARME_SCRIPT_URL;
+
+      script.setAttribute('data-pagarmecheckout-app-id', publicKey);
+      script.setAttribute('data-pagarmecheckout-script', 'true');
+
+      document.body.appendChild(script);
+    });
+  }
+
+  return pagarmeScriptPromise;
 };
 
 const onCardNumberChange = () => {
   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('*')) {
     const brand = detectCardBrand(raw);
-    if (brand) form.brand = brand;
-    if (raw.length >= 4) form.last_four_digits = raw.slice(-4);
+
+    if (brand) {
+      form.brand = brand;
+    }
+
+    if (raw.length >= 4) {
+      form.last_four_digits = raw.slice(-4);
+    }
   }
 };
 
-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;
+  const publicKey = getPagarmePublicKey();
 
-  if (!encryptionKey) {
-    throw new Error('PAGARME_ENCRYPTION_KEY não configurada');
+  if (!publicKey) {
+    throw new Error('PAGARME_PUBLIC_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, '') : '';
+  return new Promise((resolve, reject) => {
+    const success = (data) => {
+      const token = getPagarmeTokenFromResponse(data);
 
-  const card = {
-    card_holder_name: form.holder_name,
-    card_expiration_date: form.expiration,
-    card_number: rawCardNumber,
-    card_cvv: rawCvv,
-  };
+      if (!token) {
+        reject(new Error('Token do cartão não retornado pela Pagar.me.'));
+        return false;
+      }
 
-  const validations = window.pagarme?.validate?.({ card });
-  if (validations?.card) {
-    if (!validations.card.card_number) {
-      throw new Error(t('profile.payments.invalid_card_number'));
-    }
+      resolve(token);
+      return false;
+    };
 
-    if (!validations.card.card_holder_name) {
-      throw new Error(t('profile.payments.holder_name'));
-    }
+    const fail = (error) => {
+      reject(error);
+      return false;
+    };
 
-    if (!validations.card.card_expiration_date) {
-      throw new Error(t('profile.payments.expired_card'));
-    }
+    window.PagarmeCheckout.init(success, fail);
 
-    if (!validations.card.card_cvv) {
-      throw new Error(t('profile.payments.cvv'));
+    if (!pagarmeFormRef.value) {
+      reject(new Error('Formulário de tokenização não encontrado.'));
+      return;
     }
+
+    pagarmeFormRef.value.dispatchEvent(new Event('submit', {
+      bubbles: true,
+      cancelable: true,
+    }));
+  });
+};
+
+const validateExpiration = (val) => {
+  if (!val) {
+    return true;
   }
 
-  const client = await window.pagarme.client.connect({ encryption_key: encryptionKey });
-  return client.security.encrypt(card);
+  if (!validateCardExpiration(val)) {
+    return t('profile.payments.expired_card');
+  }
+
+  return true;
 };
 
+//
+
 const onOKClick = async () => {
   const valid = await formRef.value?.validate();
-  if (!valid) return;
+
+  if (!valid) {
+    return;
+  }
 
   if (props.paymentMethod) {
-    const payload = buildPayload();
+    const payload = {
+      brand: form.brand,
+      card_name: form.card_name,
+      client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
+      expiration: form.expiration,
+      holder_name: form.holder_name,
+      is_active: form.is_active,
+      last_four_digits: form.last_four_digits,
+    }
+
     await submitForm(() => updateClientPaymentMethod(props.paymentMethod.id, payload));
+
     return;
   }
 
@@ -347,18 +571,24 @@ const onOKClick = async () => {
 
   try {
     const token = await tokenizeCard();
-    const payload = buildPayload({
+
+    const payload = {
+      client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
+      expiration: form.expiration,
+      holder_name: form.holder_name,
+      last_four_digits: form.last_four_digits,
       token,
-      card_number: null,
-      cvv: null,
-    });
+    };
 
     await submitForm(() => createClientPaymentMethod(payload));
   } catch (error) {
     console.error('Erro ao tokenizar o cartão:', error);
+
     $q.notify({
+      message: error instanceof Error
+        ? error.message
+        : 'Não foi possível tokenizar o cartão.',
       type: 'negative',
-      message: error instanceof Error ? error.message : 'Não foi possível tokenizar o cartão.',
     });
   } finally {
     tokenizing.value = false;
@@ -383,6 +613,15 @@ const onOKClick = async () => {
   color: white;
 }
 
+.pagarme-token-form {
+  height: 0;
+  opacity: 0;
+  overflow: hidden;
+  pointer-events: none;
+  position: absolute;
+  width: 0;
+}
+
 .nfc-icon {
   opacity: 0.85;
 }
@@ -466,4 +705,4 @@ const onOKClick = async () => {
   font-size: 16px;
   font-weight: 700;
 }
-</style>
+</style>