Browse Source

add the tshield need finishe the webhook

gdias 1 month ago
parent
commit
24439dc0b5
5 changed files with 719 additions and 119 deletions
  1. 19 0
      package-lock.json
  2. 1 0
      package.json
  3. 0 10
      src/lib/components/users/UserCreateModal.svelte
  4. 457 72
      src/routes/+page.svelte
  5. 242 37
      src/routes/register/+page.svelte

+ 19 - 0
package-lock.json

@@ -13,6 +13,7 @@
 				"@schedule-x/theme-default": "^3.3.0",
 				"flatpickr": "^4.6.13",
 				"layerchart": "^1.0.12",
+				"qr-code-styling": "^1.9.2",
 				"temporal-polyfill": "^0.3.0"
 			},
 			"devDependencies": {
@@ -2878,6 +2879,24 @@
 				}
 			}
 		},
+		"node_modules/qr-code-styling": {
+			"version": "1.9.2",
+			"resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz",
+			"integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==",
+			"license": "MIT",
+			"dependencies": {
+				"qrcode-generator": "^1.4.4"
+			},
+			"engines": {
+				"node": ">=18.18.0"
+			}
+		},
+		"node_modules/qrcode-generator": {
+			"version": "1.5.2",
+			"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz",
+			"integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==",
+			"license": "MIT"
+		},
 		"node_modules/queue-microtask": {
 			"version": "1.2.3",
 			"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

+ 1 - 0
package.json

@@ -31,6 +31,7 @@
 		"@schedule-x/theme-default": "^3.3.0",
 		"flatpickr": "^4.6.13",
 		"layerchart": "^1.0.12",
+		"qr-code-styling": "^1.9.2",
 		"temporal-polyfill": "^0.3.0"
 	}
 }

+ 0 - 10
src/lib/components/users/UserCreateModal.svelte

@@ -22,7 +22,6 @@
   let state = '';
   let zip = '';
   let country = 'BR';
-  let kyc = false;
 
   let errors = { name: '', email: '', password: '', confirmPassword: '' };
   let lastResetToken = 0;
@@ -40,7 +39,6 @@
     state = '';
     zip = '';
     country = 'BR';
-    kyc = false;
     errors = { name: '', email: '', password: '', confirmPassword: '' };
   }
 
@@ -68,7 +66,6 @@
         birthdateUnix = Math.floor(dateObj.getTime() / 1000);
       }
     }
-    const kycNum = kyc ? 1 : 0;
     const sanitizedCpf = cpf ? cpf.replace(/\D+/g, '') : undefined;
     const payload = {
       name: name?.trim() ?? '',
@@ -81,7 +78,6 @@
       state: state?.trim() ?? '',
       zip: zip?.trim() ?? '',
       country: country?.trim() ?? '',
-      kyc: kycNum,
       birthdate: birthdateUnix,
       cpf: sanitizedCpf
     };
@@ -231,12 +227,6 @@
             </div>
           </div>
 
-          <div>
-            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">KYC verificado</label>
-            <div class="mt-2">
-              <input type="checkbox" bind:checked={kyc} class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" />
-            </div>
-          </div>
         </div>
       </div>
 

+ 457 - 72
src/routes/+page.svelte

@@ -1,7 +1,7 @@
 <script>
   import { goto } from '$app/navigation';
   import { browser } from '$app/environment';
-  import { onMount, onDestroy } from 'svelte';
+  import { onMount, tick } from 'svelte';
   import { authToken, companyId as companyIdStore } from '$lib/utils/stores';
 
   const apiUrl = import.meta.env.VITE_API_URL;
@@ -13,6 +13,273 @@
   let success = '';
   let successTimeout;
 
+  const kycStatusEndpoint = `${apiUrl}/company/user/kyc/status`;
+  const KYC_POLL_INTERVAL_MS = 10_000;
+  const LOGIN_KYC_PENDING_KEY = 'login_kyc_pending_state';
+  const LOGIN_KYC_PENDING_TTL_MS = 1000 * 60 * 30;
+
+  let kycMode = null;
+  let kycLink = '';
+  let kycNumberToken = '';
+  let kycStatusMessage = '';
+  let kycStatusCode = null;
+  let kycError = '';
+  let qrCodeContainer;
+  let qrCodeInstance = null;
+  let kycPollingId = null;
+  let isCheckingKyc = false;
+
+  function showTimedSuccess(message) {
+    success = message;
+    if (successTimeout) {
+      clearTimeout(successTimeout);
+    }
+    successTimeout = setTimeout(() => {
+      success = '';
+    }, 4000);
+  }
+
+  function savePendingState(state) {
+    if (!browser) return;
+    try {
+      const payload = { ...state, savedAt: Date.now() };
+      localStorage.setItem(LOGIN_KYC_PENDING_KEY, JSON.stringify(payload));
+    } catch (err) {
+      console.warn('Não foi possível salvar o estado de pendência de KYC:', err);
+    }
+  }
+
+  function loadPendingState() {
+    if (!browser) return null;
+    try {
+      const raw = localStorage.getItem(LOGIN_KYC_PENDING_KEY);
+      if (!raw) return null;
+      const data = JSON.parse(raw);
+      if (data?.savedAt && Date.now() - data.savedAt > LOGIN_KYC_PENDING_TTL_MS) {
+        localStorage.removeItem(LOGIN_KYC_PENDING_KEY);
+        return null;
+      }
+      return data;
+    } catch (err) {
+      console.warn('Não foi possível restaurar o estado de pendência de KYC:', err);
+      return null;
+    }
+  }
+
+  function clearPendingState() {
+    if (!browser) return;
+    try {
+      localStorage.removeItem(LOGIN_KYC_PENDING_KEY);
+    } catch (err) {
+      console.warn('Não foi possível limpar o estado de pendência de KYC:', err);
+    }
+  }
+
+  function extractKycLinkData(data = {}) {
+    return {
+      link: data?.link ?? data?.tshield?.link,
+      numberToken: data?.numberToken ?? data?.tshield?.numberToken ?? data?.tshield?.number
+    };
+  }
+
+  async function handleLoginErrorResponse(status, payload) {
+    const code = payload?.code;
+    const message = payload?.msg ?? payload?.message;
+
+    if (status === 401 || code === 'E_VALIDATE') {
+      error = message ?? 'Credenciais inválidas.';
+      return;
+    }
+
+    if (status === 403 && code === 'E_KYC') {
+      const reason = payload?.data?.reason;
+      if (reason === 'KYC_PJ_PENDING') {
+        enterPjPending(message ?? 'Necessário finalizar análise PJ ou contatar o suporte.');
+        return;
+      }
+
+      const { link, numberToken } = extractKycLinkData(payload?.data ?? {});
+      if (link && numberToken) {
+        await enterPfPending(link, numberToken, message ?? 'KYC pendente. Conclua pelo link disponibilizado.');
+        return;
+      }
+
+      if (reason === 'KYC_PF_MISSING_DOCUMENT') {
+        error = message ?? 'CPF não cadastrado. Contate o suporte para concluir a verificação.';
+        return;
+      }
+
+      error = message ?? 'KYC pendente. Entre em contato com o suporte.';
+      return;
+    }
+
+    if (status === 502 || code === 'E_EXTERNAL') {
+      error = message ?? 'Não foi possível gerar o link de verificação. Tente novamente.';
+      return;
+    }
+
+    if (code === 'E_VALIDATE') {
+      error = message ?? 'Credenciais inválidas.';
+      return;
+    }
+
+    error = message ?? 'Falha ao autenticar. Tente novamente.';
+  }
+
+  function stopKycPolling() {
+    if (kycPollingId) {
+      clearInterval(kycPollingId);
+      kycPollingId = null;
+    }
+    isCheckingKyc = false;
+  }
+
+  function resetKycState() {
+    stopKycPolling();
+    kycMode = null;
+    kycLink = '';
+    kycNumberToken = '';
+    kycStatusMessage = '';
+    kycStatusCode = null;
+    kycError = '';
+    if (qrCodeContainer) {
+      qrCodeContainer.innerHTML = '';
+    }
+  }
+
+  async function renderQrCode(link) {
+    if (!browser || !link) return;
+    if (!qrCodeInstance) {
+      const module = await import('qr-code-styling');
+      const QRCodeStyles = module.default ?? module;
+      qrCodeInstance = new QRCodeStyles({
+        width: 240,
+        height: 240,
+        type: 'svg',
+        data: link,
+        dotsOptions: {
+          color: '#0f172a',
+          type: 'rounded'
+        },
+        backgroundOptions: {
+          color: '#ffffff'
+        }
+      });
+    } else {
+      qrCodeInstance.update({ data: link });
+    }
+    await tick();
+    if (qrCodeContainer) {
+      qrCodeContainer.innerHTML = '';
+      qrCodeInstance.append(qrCodeContainer);
+    }
+  }
+
+  function startKycPolling() {
+    stopKycPolling();
+    checkKycStatus();
+    kycPollingId = setInterval(checkKycStatus, KYC_POLL_INTERVAL_MS);
+  }
+
+  async function checkKycStatus() {
+    if (!kycNumberToken || isCheckingKyc) return;
+    isCheckingKyc = true;
+    try {
+      const res = await fetch(kycStatusEndpoint, {
+        method: 'POST',
+        headers: { 'content-type': 'application/json' },
+        body: JSON.stringify({ numberToken: kycNumberToken })
+      });
+      const raw = await res.text();
+      let body = null;
+      if (raw) {
+        try {
+          body = JSON.parse(raw);
+        } catch (err) {
+          console.error('Resposta inválida do endpoint de status do KYC:', err);
+        }
+      }
+      if (!res.ok) {
+        const message = body?.message ?? body?.msg ?? 'Falha ao consultar status do KYC.';
+        throw new Error(message);
+      }
+
+      const resolvedStatus = Number(body?.status);
+      if (!Number.isNaN(resolvedStatus)) {
+        kycStatusCode = resolvedStatus;
+        if (resolvedStatus === 1) {
+          kycStatusMessage = 'KYC concluído com sucesso. Faça login novamente.';
+          kycError = '';
+          clearPendingState();
+          stopKycPolling();
+        } else if (resolvedStatus === 2) {
+          kycStatusMessage = 'Falha na validação do KYC. Entre em contato com o suporte.';
+          clearPendingState();
+          stopKycPolling();
+        } else {
+          kycStatusMessage = 'Aguardando validação do KYC...';
+        }
+      } else {
+        kycStatusMessage = 'Aguardando validação do KYC...';
+      }
+    } catch (err) {
+      console.error('Erro ao verificar status do KYC:', err);
+      kycError = err?.message ?? 'Não foi possível verificar o status do KYC.';
+    } finally {
+      isCheckingKyc = false;
+    }
+  }
+
+  async function enterPfPending(link, numberToken, message, options = {}) {
+    const { persist = true } = options;
+    resetKycState();
+    kycMode = 'PF_PENDING';
+    kycLink = link;
+    kycNumberToken = numberToken;
+    kycStatusMessage = message ?? 'KYC pendente. Conclua pelo QR Code.';
+    kycStatusCode = null;
+    kycError = '';
+    error = '';
+    success = '';
+    if (persist) {
+      savePendingState({
+        mode: 'PF_PENDING',
+        link,
+        numberToken,
+        message: kycStatusMessage
+      });
+    }
+    await renderQrCode(link);
+    startKycPolling();
+  }
+
+  function enterPjPending(message, options = {}) {
+    const { persist = true } = options;
+    resetKycState();
+    kycMode = 'PJ_PENDING';
+    kycStatusMessage = message ?? 'Necessário finalizar análise PJ ou contatar o suporte.';
+    error = '';
+    success = '';
+    if (persist) {
+      savePendingState({
+        mode: 'PJ_PENDING',
+        message: kycStatusMessage
+      });
+    }
+  }
+
+  function handleBackToLogin(message = '') {
+    resetKycState();
+    clearPendingState();
+    if (message) {
+      showTimedSuccess(message);
+    }
+  }
+
+  function handleCancelKycFlow() {
+    handleBackToLogin('');
+  }
+
   function setDark(enabled) {
     if (enabled) document.documentElement.classList.add('dark');
     else document.documentElement.classList.remove('dark');
@@ -34,29 +301,42 @@
     const onStorage = (e) => {
       if (e.key === 'darkmode') applyDarkModeFromStorage();
     };
-    window.addEventListener('storage', onStorage);
+    if (browser) window.addEventListener('storage', onStorage);
     try {
       const stored = localStorage.getItem('registrationSuccessMessage');
       if (stored) {
-        success = stored;
         localStorage.removeItem('registrationSuccessMessage');
-        successTimeout = setTimeout(() => success = '', 4000);
+        showTimedSuccess(stored);
       }
     } catch (err) {
       console.warn('Falha ao recuperar mensagem de sucesso:', err);
     }
-    onDestroy(() => {
-      window.removeEventListener('storage', onStorage);
+
+    const restorePendingState = async () => {
+      const pending = loadPendingState();
+      if (!pending) return;
+      if (pending.mode === 'PF_PENDING' && pending.link && pending.numberToken) {
+        await enterPfPending(pending.link, pending.numberToken, pending.message, { persist: false });
+      } else if (pending.mode === 'PJ_PENDING') {
+        enterPjPending(pending.message, { persist: false });
+      }
+    };
+    void restorePendingState();
+
+    return () => {
+      if (browser) window.removeEventListener('storage', onStorage);
       if (successTimeout) {
         clearTimeout(successTimeout);
       }
-    });
+    };
   });
 
   async function onSubmit(e) {
     e.preventDefault();
     error = '';
+    success = '';
     loading = true;
+    resetKycState();
     try {
       if (!email || !password) {
         throw new Error('Preencha e-mail e senha.');
@@ -72,19 +352,25 @@
         body: JSON.stringify({ email, password })
       });
 
-      if (!res.ok) {
-        let msg = 'Credenciais inválidas.';
+      const raw = await res.text();
+      let payload = null;
+      if (raw) {
         try {
-          const err = await res.json();
-          if (err?.message) msg = err.message;
-        } catch {}
-        throw new Error(msg);
+          payload = JSON.parse(raw);
+        } catch (err) {
+          console.error('Resposta inválida do login:', err);
+        }
+      }
+
+      if (!res.ok) {
+        await handleLoginErrorResponse(res.status, payload);
+        return;
       }
 
-      const payload = await res.json();
       const statusOk = payload?.status === 'ok' && payload?.code === 'S_OK';
       if (!statusOk) {
-        throw new Error(payload?.msg ?? 'Falha ao autenticar.');
+        await handleLoginErrorResponse(res.status, payload);
+        return;
       }
 
       const token = payload?.data?.token;
@@ -106,6 +392,8 @@
       }
       authToken.set(token);
 
+      clearPendingState();
+
       if (browser) {
         try {
           localStorage.removeItem('registrationSuccessMessage');
@@ -125,68 +413,165 @@
 
 <div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
   <div class="w-full max-w-md">
-    <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-8">
-      <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Entrar</h1>
-      <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Acesse sua conta para continuar.</p>
-
-      {#if error}
-        <div class="mb-4 rounded border border-red-300 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{error}</div>
-      {/if}
-      {#if success}
-        <div class="mb-4 rounded border border-green-300 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 px-3 py-2 text-sm">{success}</div>
-      {/if}
-
-      <form on:submit|preventDefault={onSubmit} class="space-y-4">
-        <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="email">E-mail</label>
-          <input
-            id="email"
-            name="email"
-            type="email"
-            bind:value={email}
-            class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-            placeholder="seu@email.com"
-            autocomplete="email"
-            required
-          />
+    {#if kycMode === 'PF_PENDING'}
+      <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-8 space-y-6">
+        <div class="text-center">
+          <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Validação necessária</h1>
+          <p class="text-sm text-gray-600 dark:text-gray-400 mt-2">
+            Escaneie o QR Code com o celular e conclua a validação facial no link da TShield.
+          </p>
+        </div>
+
+        <div class="space-y-3 text-sm text-gray-600 dark:text-gray-300">
+          <p>1. Abra a câmera do celular e aponte para o QR Code.</p>
+          <p>2. Siga as instruções para a verificação facial.</p>
+          <p>3. Aguarde alguns minutos — verificamos o status automaticamente.</p>
+        </div>
+
+        <div class="flex justify-center">
+          <div class="p-4 border border-dashed border-gray-300 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-800/50">
+            <div
+              class="w-56 h-56 flex items-center justify-center text-gray-500 dark:text-gray-400"
+              bind:this={qrCodeContainer}
+            >
+              {#if !kycLink}
+                Gerando QR Code...
+              {/if}
+            </div>
+          </div>
+        </div>
+
+        <div class="rounded border border-blue-100 dark:border-blue-900/40 bg-blue-50 dark:bg-blue-900/20 px-3 py-2 text-sm text-blue-800 dark:text-blue-100">
+          {kycStatusMessage || 'Aguardando validação do KYC...'}
         </div>
 
-        <div>
-          <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="password">Senha</label>
-          <input
-            id="password"
-            name="password"
-            type="password"
-            bind:value={password}
-            class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-            placeholder="••••••••"
-            autocomplete="current-password"
-            required
-          />
+        {#if kycError}
+          <div class="rounded border border-red-200 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{kycError}</div>
+        {/if}
+
+        <div class="space-y-2 text-sm text-gray-600 dark:text-gray-300">
+          <p>Se preferir, abra o link diretamente:</p>
+          <a
+            class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-500 text-sm"
+            href={kycLink}
+            target="_blank"
+            rel="noreferrer"
+          >
+            <span>📱 Abrir link de verificação</span>
+          </a>
         </div>
 
-        <button
-          class="w-full inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
-          disabled={loading}
-          type="submit"
-        >
-          {#if loading}
-            Entrando...
-          {:else}
-            Entrar
+        <div class="flex flex-col sm:flex-row gap-3">
+          <button
+            type="button"
+            class="w-full inline-flex items-center justify-center rounded border border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+            on:click={handleCancelKycFlow}
+          >
+            Voltar ao login
+          </button>
+          {#if kycStatusCode === 1}
+            <button
+              type="button"
+              class="w-full inline-flex items-center justify-center rounded bg-green-600 hover:bg-green-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-green-500"
+              on:click={() => handleBackToLogin('KYC concluído com sucesso. Faça login novamente.')}
+            >
+              Concluir
+            </button>
           {/if}
-        </button>
-      </form>
-    </div>
-
-    <p class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
-      Esqueceu a senha? <a href="#" class="text-blue-600 hover:text-blue-500">Fale com o suporte</a>
-    </p>
-    <div class="mt-3">
-      <a href="/register" class="block w-full text-center rounded border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 font-medium px-4 py-2">
-        Ainda não tem conta? Crie agora
-      </a>
-    </div>
+        </div>
+      </div>
+    {:else if kycMode === 'PJ_PENDING'}
+      <div class="bg-white dark:bg-gray-800 border border-amber-300 dark:border-amber-500/50 rounded-lg shadow-sm p-8 space-y-6">
+        <div class="text-center space-y-2">
+          <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Análise PJ em andamento</h1>
+          <p class="text-sm text-gray-700 dark:text-gray-300">
+            {kycStatusMessage || 'Necessário finalizar análise PJ ou contatar o suporte.'}
+          </p>
+        </div>
+
+        <div class="rounded border border-amber-200 bg-amber-50 dark:border-amber-500/40 dark:bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">
+          Nossa equipe está validando os documentos. Assim que concluir, tente acessar novamente ou fale com o suporte.
+        </div>
+
+        <div class="space-y-3">
+          <a href="mailto:suporte@toeasy.com" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-500 text-sm">
+            <span>💬 Contactar suporte</span>
+          </a>
+          <button
+            type="button"
+            class="w-full inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+            on:click={handleCancelKycFlow}
+          >
+            Voltar ao login
+          </button>
+        </div>
+      </div>
+    {:else}
+      <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-8">
+        <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Entrar</h1>
+        <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Acesse sua conta para continuar.</p>
+
+        {#if error}
+          <div class="mb-4 rounded border border-red-300 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{error}</div>
+        {/if}
+        {#if success}
+          <div class="mb-4 rounded border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-3 py-2 text-sm">
+            {success}
+          </div>
+        {/if}
+
+        <form on:submit|preventDefault={onSubmit} class="space-y-4">
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="email">E-mail</label>
+            <input
+              id="email"
+              name="email"
+              type="email"
+              bind:value={email}
+              class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+              placeholder="seu@email.com"
+              autocomplete="email"
+              required
+            />
+          </div>
+
+          <div>
+            <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="password">Senha</label>
+            <input
+              id="password"
+              name="password"
+              type="password"
+              bind:value={password}
+              class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+              placeholder="••••••••"
+              autocomplete="current-password"
+              required
+            />
+          </div>
+
+          <button
+            class="w-full inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
+            disabled={loading}
+            type="submit"
+          >
+            {#if loading}
+              Entrando...
+            {:else}
+              Entrar
+            {/if}
+          </button>
+        </form>
+      </div>
+
+      <p class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
+        Esqueceu a senha? <a href="#" class="text-blue-600 hover:text-blue-500">Fale com o suporte</a>
+      </p>
+      <div class="mt-3">
+        <a href="/register" class="block w-full text-center rounded border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 font-medium px-4 py-2">
+          Ainda não tem conta? Crie agora
+        </a>
+      </div>
+    {/if}
   </div>
   
 </div>

+ 242 - 37
src/routes/register/+page.svelte

@@ -1,7 +1,7 @@
 <script>
   import { goto } from '$app/navigation';
   import { browser } from '$app/environment';
-  import { onMount, onDestroy } from 'svelte';
+  import { onMount, onDestroy, tick } from 'svelte';
 
   const apiUrl = import.meta.env.VITE_API_URL;
 
@@ -24,11 +24,21 @@
   let stateUF = '';
   let zip = '';
   let country = 'BR';
-  let kyc = 1;
   let birthdate = '';
   let cpf = '';
-  
+  let kycLink = '';
+  let kycNumberToken = '';
+  let kycStatusCode = null;
+  let kycStatusMessage = '';
+  let kycError = '';
+  let isCheckingKyc = false;
+  let qrCodeContainer;
+  let qrCodeInstance = null;
+  let kycPollingId = null;
+
   const companyWithUserEndpoint = `${apiUrl}/company/user/create`;
+  const kycStatusEndpoint = `${apiUrl}/company/user/kyc/status`;
+  const KYC_POLL_INTERVAL_MS = 10_000;
 
   function setDark(enabled) {
     if (enabled) document.documentElement.classList.add('dark');
@@ -50,16 +60,143 @@
     const onStorage = (e) => {
       if (e.key === 'darkmode') applyDarkModeFromStorage();
     };
-    window.addEventListener('storage', onStorage);
-    onDestroy(() => {
-      window.removeEventListener('storage', onStorage);
-    });
+    if (browser) window.addEventListener('storage', onStorage);
+    return () => {
+      if (browser) window.removeEventListener('storage', onStorage);
+    };
   });
 
+  onDestroy(() => {
+    stopKycPolling();
+  });
+
+  $: if (step !== 3) {
+    stopKycPolling();
+    if (step !== 3) {
+      kycLink = '';
+      kycNumberToken = '';
+      kycStatusCode = null;
+      kycStatusMessage = '';
+      kycError = '';
+    }
+  }
+
   function digitsOnly(value) {
     return value ? value.replace(/\D/g, '') : '';
   }
 
+  async function renderQrCode(link) {
+    if (!browser || !link) return;
+    if (!qrCodeInstance) {
+      const module = await import('qr-code-styling'); // biblioteca qrcodeStyles
+      const QRCodeStyles = module.default ?? module;
+      qrCodeInstance = new QRCodeStyles({
+        width: 240,
+        height: 240,
+        type: 'svg',
+        data: link,
+        dotsOptions: {
+          color: '#0f172a',
+          type: 'rounded'
+        },
+        backgroundOptions: {
+          color: '#ffffff'
+        }
+      });
+    } else {
+      qrCodeInstance.update({ data: link });
+    }
+    await tick();
+    if (qrCodeContainer) {
+      qrCodeContainer.innerHTML = '';
+      qrCodeInstance.append(qrCodeContainer);
+    }
+  }
+
+  function stopKycPolling() {
+    if (kycPollingId) {
+      clearInterval(kycPollingId);
+      kycPollingId = null;
+    }
+    isCheckingKyc = false;
+  }
+
+  function startKycPolling() {
+    stopKycPolling();
+    checkKycStatus();
+    kycPollingId = setInterval(checkKycStatus, KYC_POLL_INTERVAL_MS);
+  }
+
+  async function startKycFlow(link, numberToken) {
+    kycLink = link;
+    kycNumberToken = numberToken;
+    kycStatusCode = null;
+    kycStatusMessage = 'Aguardando validação do KYC...';
+    kycError = '';
+    step = 3;
+    await renderQrCode(link);
+    startKycPolling();
+  }
+
+  async function checkKycStatus() {
+    if (!kycNumberToken || isCheckingKyc) return;
+    isCheckingKyc = true;
+    try {
+      const res = await fetch(kycStatusEndpoint, {
+        method: 'POST',
+        headers: { 'content-type': 'application/json' },
+        body: JSON.stringify({ numberToken: kycNumberToken })
+      });
+      const raw = await res.text();
+      let body = null;
+      if (raw) {
+        try {
+          body = JSON.parse(raw);
+        } catch (err) {
+          console.error('Resposta inválida do endpoint de status do KYC:', err);
+        }
+      }
+      if (!res.ok) {
+        const message = body?.message ?? body?.msg ?? 'Falha ao consultar status do KYC.';
+        throw new Error(message);
+      }
+
+      const resolvedStatus = Number(body?.status);
+      if (!Number.isNaN(resolvedStatus)) {
+        kycStatusCode = resolvedStatus;
+        if (resolvedStatus === 1) {
+          kycStatusMessage = 'KYC completado com sucesso';
+          success = kycStatusMessage;
+          if (browser) {
+            try {
+              localStorage.setItem('registrationSuccessMessage', success);
+            } catch (storageErr) {
+              console.warn('Não foi possível salvar o estado de sucesso:', storageErr);
+            }
+          }
+          stopKycPolling();
+        } else if (resolvedStatus === 2) {
+          kycStatusMessage = 'Falha na validação do KYC. Entre em contato com o suporte.';
+          stopKycPolling();
+        } else {
+          kycStatusMessage = 'Aguardando validação do KYC...';
+        }
+      } else {
+        kycStatusMessage = 'Aguardando validação do KYC...';
+      }
+    } catch (err) {
+      console.error('Erro ao verificar status do KYC:', err);
+      kycError = err?.message ?? 'Não foi possível verificar o status do KYC.';
+    } finally {
+      isCheckingKyc = false;
+    }
+  }
+
+  function handleReturnToLogin() {
+    stopKycPolling();
+    goto('/');
+  }
+
   async function onSubmit(e) {
     e.preventDefault();
     error = '';
@@ -104,7 +241,6 @@
         state: stateUF,
         zip,
         country,
-        kyc: Number(kyc),
         birthdate: toBirthdateEpoch(birthdate),
         cpf: digitsOnly(cpf)
       };
@@ -115,25 +251,56 @@
         body: JSON.stringify(payload)
       });
 
-      if (!res.ok) {
-        let msg = 'Falha ao criar conta.';
+      const raw = await res.text();
+      let body = null;
+      if (raw) {
         try {
-          const err = await res.json();
-          console.error('Erro retornado pelo backend:', err);
-          if (err?.message) msg = err.message;
-        } catch {}
+          body = JSON.parse(raw);
+        } catch (parseErr) {
+          console.error('Resposta inválida do endpoint /company/user/create:', parseErr);
+        }
+      }
+
+      if (!res.ok) {
+        const msg =
+          body?.message ??
+          body?.msg ??
+          body?.error ??
+          'Falha ao criar conta.';
         throw new Error(msg);
       }
 
-      success = 'Conta criada com sucesso. Você já pode entrar.';
+      /* Fluxo original de geração de QR Code desabilitado temporariamente
+      const linkFromBackend =
+        body?.link ??
+        body?.kycLink ??
+        body?.data?.link ??
+        body?.data?.kycLink;
+      const numberTokenFromBackend =
+        body?.numberToken ??
+        body?.kycNumberToken ??
+        body?.data?.numberToken ??
+        body?.data?.kycNumberToken ??
+        body?.token;
+
+      if (!linkFromBackend || !numberTokenFromBackend) {
+        throw new Error('Resposta de KYC inválida: link ou token ausente.');
+      }
+
+      await startKycFlow(linkFromBackend, numberTokenFromBackend);
+      */
+
+      const analysisMessage = 'Seus dados estão em análise e pode levar alguns minutos.';
       if (browser) {
         try {
-          localStorage.setItem('registrationSuccessMessage', success);
-        } catch (err) {
-          console.warn('Não foi possível salvar o estado de sucesso:', err);
+          localStorage.setItem('registrationSuccessMessage', analysisMessage);
+        } catch (storageErr) {
+          console.warn('Não foi possível salvar o estado de análise:', storageErr);
         }
       }
-      setTimeout(() => goto('/'), 800);
+      await goto('/');
+      loading = false;
+      return;
     } catch (err) {
       error = err?.message ?? 'Falha no cadastro, tente novamente.';
     } finally {
@@ -148,9 +315,11 @@
       <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Criar conta</h1>
       <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
         {#if step === 1}
-          Etapa 1 de 2: Dados da empresa
+          Etapa 1 de 3: Dados da empresa
+        {:else if step === 2}
+          Etapa 2 de 3: Dados do proprietário
         {:else}
-          Etapa 2 de 2: Dados do proprietário
+          Etapa 3 de 3: Validação de identidade (KYC)
         {/if}
       </p>
 
@@ -199,7 +368,7 @@
               Continuar
             {/if}
           </button>
-        {:else}
+        {:else if step === 2}
           <div>
             <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="ownerName">Nome do proprietário</label>
             <input
@@ -338,19 +507,6 @@
           </div>
 
           <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
-            <div>
-              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="kyc">KYC</label>
-              <select
-                id="kyc"
-                name="kyc"
-                bind:value={kyc}
-                class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-                required
-              >
-                <option value="1">1 (Sim)</option>
-                <option value="0">0 (Não)</option>
-              </select>
-            </div>
             <div class="sm:col-span-2">
               <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="birthdate">Data de nascimento</label>
               <input
@@ -390,12 +546,61 @@
               type="submit"
             >
               {#if loading}
-                Enviando...
+                Continuando...
               {:else}
-                Concluir cadastro
+                Continuar
               {/if}
             </button>
           </div>
+        {:else}
+          <div class="space-y-6">
+            <div class="text-center">
+              <p class="text-base font-semibold text-gray-900 dark:text-gray-100">Escaneie o QR Code para continuar</p>
+              <p class="text-sm text-gray-600 dark:text-gray-400 mt-2">
+                Use a câmera do seu celular para abrir o link seguro e concluir a verificação de identidade.
+              </p>
+            </div>
+
+            <div class="flex justify-center">
+              <div class="p-4 border border-dashed border-gray-300 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-800/50">
+                <div
+                  class="w-56 h-56 flex items-center justify-center text-gray-500 dark:text-gray-400"
+                  bind:this={qrCodeContainer}
+                >
+                  {#if !kycLink}
+                    Carregando QR Code...
+                  {/if}
+                </div>
+              </div>
+            </div>
+
+            <div class="rounded border border-blue-100 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20 px-3 py-2 text-sm text-blue-800 dark:text-blue-100">
+              {kycStatusMessage || 'Aguardando validação do KYC...'}
+            </div>
+
+            {#if kycError}
+              <div class="rounded border border-red-200 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{kycError}</div>
+            {/if}
+
+            {#if kycStatusCode === 1}
+              <button
+                type="button"
+                class="w-full inline-flex items-center justify-center rounded bg-green-600 hover:bg-green-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-green-500"
+                on:click={handleReturnToLogin}
+              >
+                Voltar para o login
+              </button>
+            {:else if kycStatusCode === 2}
+              <div class="text-center space-y-2">
+                <p class="text-red-600 dark:text-red-400 font-medium">Falha na validação do KYC. Entre em contato com o suporte.</p>
+                <p class="text-sm text-gray-500 dark:text-gray-400">O processo foi interrompido. Nossa equipe pode orientar os próximos passos.</p>
+              </div>
+            {:else}
+              <p class="text-xs text-center text-gray-500 dark:text-gray-400">
+                Verificamos o status automaticamente a cada 10 segundos. Não feche esta janela.
+              </p>
+            {/if}
+          </div>
         {/if}
       </form>
     </div>