feat(chat): identificación por correo con registro automático
- Reemplaza campo teléfono por correo electrónico en el inicio del chat - Si el correo existe: envía OTP y abre el chat - Si no existe: ofrece crear cuenta automáticamente (pide nombre) - Si rechaza registro: vuelve al formulario inicial - Cuenta nueva se crea con contraseña aleatoria Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
536377363c
commit
a693ce2527
@@ -17,6 +17,7 @@ use App\Models\Tarifas;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
@@ -27,9 +28,9 @@ class PublicChat extends Component
|
|||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
|
|
||||||
// ── Paso actual ───────────────────────────────────────────
|
// ── Paso actual ───────────────────────────────────────────
|
||||||
public string $paso = 'telefono'; // telefono | verificacion | chat
|
public string $paso = 'email'; // email | verificacion | registro | chat
|
||||||
public string $telefono = '';
|
public string $email = '';
|
||||||
public string $nombre = '';
|
public string $nombreRegistro = '';
|
||||||
public string $codigoIngresado = '';
|
public string $codigoIngresado = '';
|
||||||
public ?string $emailMascarado = null;
|
public ?string $emailMascarado = null;
|
||||||
public ?int $contactIdPending = null; // contact.id mientras espera verificacion
|
public ?int $contactIdPending = null; // contact.id mientras espera verificacion
|
||||||
@@ -52,54 +53,83 @@ class PublicChat extends Component
|
|||||||
public bool $procesandoImagen = false;
|
public bool $procesandoImagen = false;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'telefono' => 'required|min:7|max:20',
|
'email' => 'required|email|max:100',
|
||||||
'nombre' => 'nullable|max:80',
|
'nombreRegistro' => 'required|max:80',
|
||||||
'codigoIngresado' => 'nullable|digits:6',
|
'codigoIngresado' => 'nullable|digits:6',
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
// Paso 1: identificar usuario
|
// Paso 1: buscar por correo
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function iniciar(): void
|
public function iniciar(): void
|
||||||
{
|
{
|
||||||
$this->validate(['telefono' => 'required|min:7|max:20', 'nombre' => 'nullable|max:80']);
|
$this->validate(['email' => 'required|email|max:100']);
|
||||||
|
|
||||||
|
$user = User::where('email', $this->email)->first();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
$this->paso = 'registro';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$contact = ChatContact::firstOrCreate(
|
$contact = ChatContact::firstOrCreate(
|
||||||
['canal' => 'web', 'canal_id' => $this->telefono],
|
['canal' => 'web', 'canal_id' => $user->email],
|
||||||
['nombre' => $this->nombre ?: $this->telefono, 'telefono' => $this->telefono]
|
['nombre' => $user->name, 'telefono' => $user->email, 'user_id' => $user->id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->nombre && $contact->nombre !== $this->nombre) {
|
|
||||||
$contact->update(['nombre' => $this->nombre]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buscar usuario en el sistema por cédula o email
|
|
||||||
$user = null;
|
|
||||||
if (! $contact->user_id) {
|
if (! $contact->user_id) {
|
||||||
$user = User::where('cedula', $this->telefono)
|
|
||||||
->orWhere('email', $this->telefono)
|
|
||||||
->first();
|
|
||||||
if ($user) {
|
|
||||||
$contact->update(['user_id' => $user->id]);
|
$contact->update(['user_id' => $user->id]);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$user = User::find($contact->user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si se encontró el usuario: enviar OTP al correo registrado
|
|
||||||
if ($user && $user->email) {
|
|
||||||
$this->contactIdPending = $contact->id;
|
$this->contactIdPending = $contact->id;
|
||||||
$this->emailMascarado = $this->enmascararEmail($user->email);
|
$this->emailMascarado = $this->enmascararEmail($user->email);
|
||||||
$this->enviarCodigoOtp($contact->id, $user);
|
$this->enviarCodigoOtp($contact->id, $user);
|
||||||
$this->paso = 'verificacion';
|
$this->paso = 'verificacion';
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sin cuenta registrada: acceso como invitado (sin saldo ni compras)
|
// ─────────────────────────────────────────────────────────
|
||||||
$this->abrirChat($contact);
|
// Paso 2a: crear cuenta nueva
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function crearCuenta(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'email' => 'required|email|max:100',
|
||||||
|
'nombreRegistro' => 'required|max:80',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $this->nombreRegistro,
|
||||||
|
'email' => $this->email,
|
||||||
|
'password' => \Illuminate\Support\Facades\Hash::make(\Illuminate\Support\Str::random(16)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contact = ChatContact::create([
|
||||||
|
'canal' => 'web',
|
||||||
|
'canal_id' => $user->email,
|
||||||
|
'nombre' => $user->name,
|
||||||
|
'telefono' => $user->email,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->contactIdPending = $contact->id;
|
||||||
|
$this->emailMascarado = $this->enmascararEmail($user->email);
|
||||||
|
$this->enviarCodigoOtp($contact->id, $user);
|
||||||
|
$this->paso = 'verificacion';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function rechazarRegistro(): void
|
||||||
|
{
|
||||||
|
$this->email = '';
|
||||||
|
$this->nombreRegistro = '';
|
||||||
|
$this->paso = 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
// Paso 2b: verificar código OTP
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private function enviarCodigoOtp(int $contactId, User $user): void
|
private function enviarCodigoOtp(int $contactId, User $user): void
|
||||||
{
|
{
|
||||||
$codigo = (string) random_int(100000, 999999);
|
$codigo = (string) random_int(100000, 999999);
|
||||||
@@ -112,10 +142,6 @@ class PublicChat extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────
|
|
||||||
// Paso 2: verificar código OTP
|
|
||||||
// ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public function verificarCodigo(): void
|
public function verificarCodigo(): void
|
||||||
{
|
{
|
||||||
$this->validate(['codigoIngresado' => 'required|digits:6']);
|
$this->validate(['codigoIngresado' => 'required|digits:6']);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
class="w-full flex flex-col bg-[#111b21]"
|
class="w-full flex flex-col bg-[#111b21]"
|
||||||
style="height: 100vh; height: 100dvh; height: -webkit-fill-available; max-width: 480px; margin: 0 auto;">
|
style="height: 100vh; height: 100dvh; height: -webkit-fill-available; max-width: 480px; margin: 0 auto;">
|
||||||
|
|
||||||
{{-- ══ PASO 1: Formulario de inicio ══ --}}
|
{{-- ══ PASO 1: Ingreso de correo ══ --}}
|
||||||
@if ($paso === 'telefono')
|
@if ($paso === 'email')
|
||||||
<div class="flex flex-col items-center justify-center flex-1 px-5 safe-top safe-bottom safe-left safe-right">
|
<div class="flex flex-col items-center justify-center flex-1 px-5 safe-top safe-bottom safe-left safe-right">
|
||||||
<div class="w-full bg-[#202c33] rounded-2xl shadow-2xl p-7">
|
<div class="w-full bg-[#202c33] rounded-2xl shadow-2xl p-7">
|
||||||
|
|
||||||
@@ -14,37 +14,74 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-white text-2xl font-semibold">Chat en vivo</h1>
|
<h1 class="text-white text-2xl font-semibold">Chat en vivo</h1>
|
||||||
<p class="text-[#8696a0] text-sm mt-1 text-center">Ingresa tu número para comenzar</p>
|
<p class="text-[#8696a0] text-sm mt-1 text-center">Ingresa tu correo para comenzar</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form wire:submit.prevent="iniciar" class="space-y-4">
|
<form wire:submit.prevent="iniciar" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#8696a0] text-xs font-medium mb-1.5 uppercase tracking-wider">Teléfono *</label>
|
<label class="block text-[#8696a0] text-xs font-medium mb-1.5 uppercase tracking-wider">Correo electrónico *</label>
|
||||||
<input wire:model.defer="telefono" type="tel" inputmode="tel" autocomplete="tel"
|
<input wire:model.defer="email" type="email" inputmode="email" autocomplete="email"
|
||||||
placeholder="Ej: 3001234567"
|
placeholder="tucorreo@ejemplo.com"
|
||||||
class="w-full bg-[#2a3942] text-white rounded-xl px-4 py-3.5 outline-none border border-transparent focus:border-[#00a884] transition placeholder-[#8696a0]"
|
|
||||||
style="color-scheme: dark;">
|
|
||||||
@error('telefono') <span class="text-red-400 text-xs mt-1 block">{{ $message }}</span> @enderror
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-[#8696a0] text-xs font-medium mb-1.5 uppercase tracking-wider">Nombre (opcional)</label>
|
|
||||||
<input wire:model.defer="nombre" type="text" autocomplete="name"
|
|
||||||
placeholder="Tu nombre"
|
|
||||||
class="w-full bg-[#2a3942] text-white rounded-xl px-4 py-3.5 outline-none border border-transparent focus:border-[#00a884] transition placeholder-[#8696a0]"
|
class="w-full bg-[#2a3942] text-white rounded-xl px-4 py-3.5 outline-none border border-transparent focus:border-[#00a884] transition placeholder-[#8696a0]"
|
||||||
style="color-scheme: dark;">
|
style="color-scheme: dark;">
|
||||||
|
@error('email') <span class="text-red-400 text-xs mt-1 block">{{ $message }}</span> @enderror
|
||||||
</div>
|
</div>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="w-full bg-[#00a884] active:bg-[#06cf9c] text-white font-semibold rounded-xl transition shadow-lg"
|
class="w-full bg-[#00a884] active:bg-[#06cf9c] text-white font-semibold rounded-xl transition shadow-lg"
|
||||||
style="min-height: 48px;">
|
style="min-height: 48px;">
|
||||||
<span wire:loading.remove wire:target="iniciar">Comenzar chat →</span>
|
<span wire:loading.remove wire:target="iniciar">Continuar →</span>
|
||||||
<span wire:loading wire:target="iniciar">Conectando...</span>
|
<span wire:loading wire:target="iniciar">Verificando...</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ══ PASO 2: Verificación OTP ══ --}}
|
{{-- ══ PASO 2: No existe — ofrecer registro ══ --}}
|
||||||
|
@elseif ($paso === 'registro')
|
||||||
|
<div class="flex flex-col items-center justify-center flex-1 px-5 safe-top safe-bottom safe-left safe-right">
|
||||||
|
<div class="w-full bg-[#202c33] rounded-2xl shadow-2xl p-7">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center mb-6">
|
||||||
|
<div class="w-16 h-16 bg-yellow-500 rounded-full flex items-center justify-center mb-4 shadow-lg">
|
||||||
|
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-white text-lg font-semibold text-center">No encontramos tu cuenta</h1>
|
||||||
|
<p class="text-[#8696a0] text-sm mt-2 text-center leading-relaxed">
|
||||||
|
No existe una cuenta con el correo<br>
|
||||||
|
<span class="text-white font-medium">{{ $email }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-[#8696a0] text-sm mt-3 text-center">¿Deseas crear una cuenta?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit.prevent="crearCuenta" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[#8696a0] text-xs font-medium mb-1.5 uppercase tracking-wider">Tu nombre *</label>
|
||||||
|
<input wire:model.defer="nombreRegistro" type="text" autocomplete="name"
|
||||||
|
placeholder="¿Cómo te llamas?"
|
||||||
|
class="w-full bg-[#2a3942] text-white rounded-xl px-4 py-3.5 outline-none border border-transparent focus:border-[#00a884] transition placeholder-[#8696a0]"
|
||||||
|
style="color-scheme: dark;">
|
||||||
|
@error('nombreRegistro') <span class="text-red-400 text-xs mt-1 block">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-[#00a884] active:bg-[#06cf9c] text-white font-semibold rounded-xl transition shadow-lg"
|
||||||
|
style="min-height: 48px;">
|
||||||
|
<span wire:loading.remove wire:target="crearCuenta">Sí, crear cuenta →</span>
|
||||||
|
<span wire:loading wire:target="crearCuenta">Creando cuenta...</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button wire:click="rechazarRegistro" type="button"
|
||||||
|
class="w-full mt-3 text-[#8696a0] text-sm hover:text-white transition py-2">
|
||||||
|
No, volver
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ══ PASO 4: Verificación OTP ══ --}}
|
||||||
@elseif ($paso === 'verificacion')
|
@elseif ($paso === 'verificacion')
|
||||||
<div class="flex flex-col items-center justify-center flex-1 px-5 safe-top safe-bottom safe-left safe-right">
|
<div class="flex flex-col items-center justify-center flex-1 px-5 safe-top safe-bottom safe-left safe-right">
|
||||||
<div class="w-full bg-[#202c33] rounded-2xl shadow-2xl p-7">
|
<div class="w-full bg-[#202c33] rounded-2xl shadow-2xl p-7">
|
||||||
@@ -92,7 +129,7 @@
|
|||||||
<span wire:loading.remove wire:target="reenviarCodigo">¿No llegó? Reenviar código</span>
|
<span wire:loading.remove wire:target="reenviarCodigo">¿No llegó? Reenviar código</span>
|
||||||
<span wire:loading wire:target="reenviarCodigo">Enviando...</span>
|
<span wire:loading wire:target="reenviarCodigo">Enviando...</span>
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="$set('paso', 'telefono')" type="button"
|
<button wire:click="$set('paso', 'email')" type="button"
|
||||||
class="text-[#8696a0] text-xs hover:text-white">
|
class="text-[#8696a0] text-xs hover:text-white">
|
||||||
Volver
|
Volver
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user