gdias vor 1 Monat
Ursprung
Commit
8de304bc96

+ 241 - 50
src/lib/components/wallet/PaymentMenu.svelte

@@ -1,46 +1,179 @@
 <script>
   import { createEventDispatcher } from 'svelte';
   import { fly, fade } from 'svelte/transition';
-  import checkIcon from '$lib/assets/icons/checkIcon.svg?raw';
+  import { tick } from 'svelte';
+  import { get } from 'svelte/store';
+  import { authToken } from '$lib/utils/stores.js';
 
   export let isOpen = false;
   export let onClose = () => {};
-  export let onConfirm = (method) => {};
+  export let onConfirm = () => {};
 
   const dispatch = createEventDispatcher();
+  const apiUrl = import.meta.env.VITE_API_URL;
 
-  let selected = 'Cartão de Crédito';
-  const methods = [
-    {
-      id: 'card',
-      label: 'Cartão de Crédito',
-      icon: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' class='w-5 h-5'><path d='M2.25 7.5A2.25 2.25 0 014.5 5.25h15a2.25 2.25 0 012.25 2.25v9A2.25 2.25 0 0119.5 18.75h-15A2.25 2.25 0 012.25 16.5v-9zM3.75 9h16.5V7.5a.75.75 0 00-.75-.75h-15a.75.75 0 00-.75.75V9z'/></svg>`
-    },
-    {
-      id: 'pix',
-      label: 'Pix',
-      icon: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' class='w-5 h-5'><path d='M15.75 2.25a3 3 0 012.121.879l3 3a3 3 0 010 4.242l-8.25 8.25a3 3 0 01-4.242 0l-3-3a3 3 0 010-4.242l8.25-8.25A3 3 0 0115.75 2.25z'/></svg>`
-    },
-    {
-      id: 'crypto',
-      label: 'Criptomoeda',
-      icon: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' class='w-5 h-5'><path d='M12 2.25c5.385 0 9.75 4.365 9.75 9.75S17.385 21.75 12 21.75 2.25 17.385 2.25 12 6.615 2.25 12 2.25zM9 8.25h4.125a2.625 2.625 0 010 5.25H9V8.25zm1.5 3.75h2.625a1.125 1.125 0 100-2.25H10.5v2.25zM9 14.25h4.5a2.25 2.25 0 110 4.5H9v-4.5zm1.5 3h3a.75.75 0 000-1.5h-3v1.5z'/></svg>`
+  let amount = '';
+  let errorMessage = '';
+  let loading = false;
+  let pixCode = '';
+  let copyFeedback = '';
+  let pixStep = 'form';
+  let qrContainer;
+  let qrInstance;
+  let prevIsOpen = false;
+
+  function resetState() {
+    amount = '';
+    errorMessage = '';
+    loading = false;
+    pixCode = '';
+    copyFeedback = '';
+    pixStep = 'form';
+    if (qrContainer) {
+      qrContainer.innerHTML = '';
+    }
+    qrInstance = null;
+  }
+
+  $: {
+    if (isOpen && !prevIsOpen) {
+      resetState();
+    }
+    if (!isOpen && prevIsOpen) {
+      resetState();
     }
-  ];
+    prevIsOpen = isOpen;
+  }
 
   function close() {
+    resetState();
     onClose();
     dispatch('close');
   }
 
-  function confirm() {
-    onConfirm(selected);
-    dispatch('confirm', { method: selected });
+  function formatCurrencyBRL(value) {
+    return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(value || 0));
+  }
+
+  async function renderQrCode(code) {
+    if (!code) return;
+    try {
+      if (!qrInstance) {
+        const module = await import('qr-code-styling');
+        const QRCodeStyles = module.default ?? module;
+        qrInstance = new QRCodeStyles({
+          width: 220,
+          height: 220,
+          type: 'svg',
+          data: code,
+          dotsOptions: { color: '#0f172a', type: 'rounded' },
+          backgroundOptions: { color: '#ffffff' }
+        });
+      } else {
+        qrInstance.update({ data: code });
+      }
+      await tick();
+      if (qrContainer) {
+        qrContainer.innerHTML = '';
+        qrInstance.append(qrContainer);
+      }
+    } catch (err) {
+      console.error('[Wallet] Falha ao renderizar QR Code', err);
+      errorMessage = 'Não foi possível gerar o QR Code. Utilize o código Pix copia e cola.';
+    }
+  }
+
+  function resolvePixFromResponse(body) {
+    if (!body) return '';
+    const data = body?.data ?? body;
+    return (
+      data?.pix_code ??
+      data?.pixCode ??
+      data?.pix ??
+      data?.copy_paste ??
+      data?.copyPaste ??
+      data?.payload ??
+      ''
+    );
+  }
+
+  async function handleConfirm() {
+    if (loading) return;
+    const numericAmount = Number(amount);
+    if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
+      errorMessage = 'Informe um valor em reais maior que zero.';
+      return;
+    }
+    const token = get(authToken);
+    if (!token) {
+      errorMessage = 'Sessão expirada. Faça login novamente.';
+      return;
+    }
+    if (!apiUrl) {
+      errorMessage = 'Configuração de API inválida.';
+      return;
+    }
+    loading = true;
+    errorMessage = '';
+    copyFeedback = '';
+    try {
+      const res = await fetch(`${apiUrl}/wallet/pix/create`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({ value: numericAmount })
+      });
+      const raw = await res.text();
+      let body = null;
+      if (raw) {
+        try {
+          body = JSON.parse(raw);
+        } catch (err) {
+          console.error('[Wallet] Resposta inválida ao solicitar PIX', err);
+        }
+      }
+      if (!res.ok || (body?.status && body.status !== 'ok')) {
+        throw new Error(body?.msg ?? body?.message ?? `Falha ao gerar pagamento (HTTP ${res.status}).`);
+      }
+      const pix = resolvePixFromResponse(body);
+      if (!pix) {
+        throw new Error('Resposta do servidor não contém o código Pix.');
+      }
+      pixCode = pix;
+      pixStep = 'result';
+      await renderQrCode(pixCode);
+      const detail = { method: 'Pix', amount: numericAmount, pixCode, shouldClose: false };
+      onConfirm?.(detail);
+      dispatch('confirm', detail);
+    } catch (err) {
+      console.error('[Wallet] Erro ao confirmar compra BRL via Pix:', err);
+      errorMessage = err?.message ?? 'Não foi possível gerar o pagamento via Pix.';
+    } finally {
+      loading = false;
+    }
+  }
+
+  async function handleCopyPix() {
+    if (!pixCode) return;
+    try {
+      await navigator.clipboard.writeText(pixCode);
+      copyFeedback = 'Código Pix copiado!';
+    } catch (err) {
+      console.error('[Wallet] Falha ao copiar Pix', err);
+      copyFeedback = 'Não foi possível copiar. Copie manualmente.';
+    }
+  }
+
+  function handleNewPayment() {
+    resetState();
   }
 </script>
 
 {#if isOpen}
-  <div class="fixed inset-0 z-50" aria-modal="true" role="dialog" aria-label="Selecionar forma de pagamento">
+  <div class="fixed inset-0 z-50" aria-modal="true" role="dialog" aria-label="Pagamento via Pix">
+
     <div
       class="absolute inset-0 bg-black/40"
       role="button"
@@ -61,7 +194,7 @@
       transition:fly={{ x: 24, duration: 200 }}
     >
       <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
-        <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Selecionar Forma de Pagamento</h3>
+        <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Pagamento via Pix</h3>
         <button
           class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
           aria-label="Fechar"
@@ -73,41 +206,99 @@
         </button>
       </div>
 
-      <div class="p-6 space-y-3 overflow-auto">
-        <div class="rounded-lg border border-amber-200 bg-amber-50 text-amber-900 px-4 py-3 text-sm">
-          <p class="font-semibold">Função não implementada ainda</p>
-          <p>
-            Esta gaveta serve apenas como preview: é possível escolher um método e fechar o fluxo,
-            porém o pagamento real ainda não foi conectado.
-          </p>
+      <div class="p-6 space-y-4 overflow-auto">
+        <div class="rounded-lg border border-blue-200 dark:border-blue-800 bg-blue-50/60 dark:bg-blue-900/20 px-4 py-3 text-sm text-blue-900 dark:text-blue-100">
+          <p class="font-semibold">Pagamento instantâneo</p>
+          <p>Informe o valor desejado para gerar um Pix copia e cola. Após a confirmação, apresentaremos o QR Code.</p>
         </div>
-        {#each methods as m}
-          <button
-            type="button"
-            class="w-full flex items-center justify-between px-4 py-3 rounded-lg border text-left transition-colors {selected === m.label ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800'}"
-            on:click={() => (selected = m.label)}
-          >
-            <span class="flex items-center gap-3 text-gray-800 dark:text-gray-100">
-              <span class="text-gray-600 dark:text-gray-300">{@html m.icon}</span>
-              {m.label}
-            </span>
-            {#if selected === m.label}
-              <span class="text-blue-600 [&>svg]:w-5 [&>svg]:h-5 [&>svg]:fill-current">{@html checkIcon}</span>
-            {/if}
-          </button>
-        {/each}
+
+        {#if errorMessage}
+          <div class="rounded border border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/30 text-sm text-red-700 dark:text-red-200 px-3 py-2">
+            {errorMessage}
+          </div>
+        {/if}
+
+        {#if pixStep === 'form'}
+          <div class="space-y-4">
+            <div class="space-y-2">
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="pix-amount">Valor em reais</label>
+              <div class="relative">
+                <span class="absolute inset-y-0 left-3 flex items-center text-gray-400">R$</span>
+                <input
+                  id="pix-amount"
+                  type="number"
+                  min="0"
+                  step="0.01"
+                  inputmode="decimal"
+                  bind:value={amount}
+                  class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 pl-9 pr-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
+                  placeholder="0,00"
+                  disabled={loading}
+                />
+              </div>
+            </div>
+
+            <div class="rounded-md border border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60 px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
+              <p class="font-semibold text-gray-700 dark:text-gray-100">Forma de pagamento</p>
+              <p>Pix (instantâneo) — o comprovante será gerado após a confirmação.</p>
+            </div>
+          </div>
+        {:else}
+          <div class="space-y-4">
+            <div class="rounded-md border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60 px-4 py-3 text-sm">
+              <p class="text-gray-600 dark:text-gray-300">Valor do pagamento</p>
+              <p class="text-lg font-semibold text-gray-900 dark:text-gray-100">{formatCurrencyBRL(Number(amount))}</p>
+            </div>
+
+            <div class="flex flex-col items-center gap-4">
+              <div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
+                <div bind:this={qrContainer} class="w-[220px] h-[220px] flex items-center justify-center" aria-label="QR Code do Pix"></div>
+              </div>
+              <div class="w-full space-y-2">
+                <label class="block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Código Pix copia e cola</label>
+                <textarea
+                  class="w-full h-28 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-sm text-gray-800 dark:text-gray-100 px-3 py-2"
+                  readonly
+                >{pixCode}</textarea>
+                <div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
+                  <button type="button" class="inline-flex items-center gap-1 px-3 py-1.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800" on:click={handleCopyPix}>
+                    Copiar código
+                  </button>
+                  {#if copyFeedback}
+                    <span>{copyFeedback}</span>
+                  {/if}
+                </div>
+              </div>
+            </div>
+          </div>
+        {/if}
       </div>
 
       <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-end gap-3">
         <button
           class="px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-100"
           on:click={close}
+          disabled={loading}
         >
-          Cancelar
-        </button>
-        <button class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white" on:click={confirm}>
-          Confirmar
+          {pixStep === 'form' ? 'Cancelar' : 'Fechar'}
         </button>
+
+        {#if pixStep === 'form'}
+          <button
+            class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60"
+            on:click={handleConfirm}
+            disabled={loading}
+          >
+            {loading ? 'Gerando Pix...' : 'Confirmar Pix'}
+          </button>
+        {:else}
+          <button
+            class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white"
+            on:click={handleNewPayment}
+          >
+            Gerar novo pagamento
+          </button>
+        {/if}
       </div>
     </div>
   </div>

+ 1 - 1
src/lib/layout/SideBar.svelte

@@ -205,7 +205,7 @@
     <div class="flex items-center justify-between text-sm p-3">
       <div class="flex items-center gap-1.5 text-gray-700 dark:text-gray-200">
         <span class="[&>svg]:w-4 [&>svg]:h-4 text-gray-500 dark:text-gray-400">{@html walletIcon}</span>
-        <span>EasyCoins</span>
+        <span>BRLA</span>
       </div>
       <div class="font-medium text-gray-900 dark:text-gray-100">{formatCoin($easyCoinBalance)}</div>
     </div>

+ 2 - 2
src/routes/wallet/+page.svelte

@@ -293,14 +293,14 @@
         class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white shadow"
         on:click={openPayment}
       >
-        Comprar EasyCoins
+        Comprar BRLA
       </button>
 
       <button
         class="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-100"
         on:click={openSell}
       >
-        Vender EasyCoins
+        Vender BRLA
       </button>
     </div>
   </div>