feat(mailbox): Implement tier-based dynamic expiration with real-time Alpine.js countdown

This commit is contained in:
idevakk
2026-03-06 01:33:15 +05:30
parent 3763847dd6
commit e79c3f79a2
3 changed files with 142 additions and 12 deletions

View File

@@ -223,7 +223,7 @@ class Mailbox extends Component
'created_ip' => request()->ip(),
'last_accessed_ip' => request()->ip(),
'last_accessed_at' => now(),
'expires_at' => now()->addDays(7), // Default expiry
'expires_at' => now()->addDays($this->getValidityDays()),
]);
$this->currentMailboxId = $mailbox->id;
@@ -262,7 +262,7 @@ class Mailbox extends Component
'created_ip' => request()->ip(),
'last_accessed_ip' => request()->ip(),
'last_accessed_at' => now(),
'expires_at' => now()->addDays(7),
'expires_at' => now()->addDays($this->getValidityDays()),
]);
$this->currentMailboxId = $mailbox->id;
@@ -270,6 +270,27 @@ class Mailbox extends Component
Session::put('current_mailbox_id', $mailbox->id);
}
/**
* Get mailbox lifespan in days based on user tier.
* Guest: 1 day, Free: 3 days, Pro: 7 days, Enterprise: 14 days.
*/
protected function getValidityDays(): int
{
$user = auth()->user();
if (! $user) {
return 1; // Guests get 24 hours
}
return match (true) {
$user->isEnterprise() => 14,
$user->isAdmin() => 14, // Assuming admins get enterprise limits
$user->isPro() => 7,
$user->isFree() => 3,
default => 3,
};
}
public function finishAutoCreation(): void
{
if ($this->getActiveMailboxesProperty()->isEmpty()) {

View File

@@ -154,18 +154,58 @@
</div>
</div>
<div class="text-[11px] font-mono text-white break-all mb-4">{{ $currentMailbox->address }}</div>
<div class="space-y-2">
<div class="space-y-2"
x-data="{
expiresAt: '{{ $currentMailbox->expires_at?->toIso8601String() }}',
created_at: '{{ $currentMailbox->created_at?->toIso8601String() }}',
timeLeft: 'Never',
percent: 100,
init() {
if (!this.expiresAt) return;
this.update();
setInterval(() => this.update(), 1000);
},
update() {
const end = new Date(this.expiresAt).getTime();
const start = new Date(this.created_at).getTime();
const now = new Date().getTime();
if (now > end) {
this.timeLeft = 'Expired';
this.percent = 0;
return;
}
// Calculate time string
const diff = end - now;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const secs = Math.floor((diff % (1000 * 60)) / 1000);
if (days > 0) {
this.timeLeft = `${days}d ${hours}h ${mins}m ${secs}s`;
} else if (hours > 0) {
this.timeLeft = `${hours}h ${mins}m ${secs}s`;
} else if (mins > 0) {
this.timeLeft = `${mins}m ${secs}s`;
} else {
this.timeLeft = `${secs}s`;
}
// Calculate percentage based on total lifespan vs remaining
const totalLifespan = end - start;
const timeRemaining = end - now;
this.percent = Math.max(0, Math.min(100, (timeRemaining / totalLifespan) * 100));
}
}">
<div class="flex items-center justify-between text-[10px]">
<span class="text-zinc-500 uppercase font-black tracking-tighter">Expires In</span>
<span class="text-pink-500 font-mono">{{ $currentMailbox->expires_at?->diffForHumans(['parts' => 2, 'short' => true]) ?? 'Never' }}</span>
<span class="text-pink-500 font-mono" x-text="timeLeft"></span>
</div>
<div class="h-1 bg-white/5 rounded-full overflow-hidden">
@php
$percent = $currentMailbox->expires_at
? max(0, min(100, (now()->diffInSeconds($currentMailbox->expires_at) / (86400 * 7)) * 100))
: 100;
@endphp
<div class="h-full bg-gradient-to-r from-pink-500 to-emerald-500" style="width: {{ $percent }}%"></div>
<div class="h-full bg-gradient-to-r from-pink-500 to-emerald-500 transition-all duration-1000 ease-linear" :style="`width: ${percent}%`"></div>
</div>
</div>
</div>
@@ -191,9 +231,43 @@
@if($mailbox->id !== $currentMailboxId)
<button wire:click="switchMailbox({{ $mailbox->id }})"
class="w-full p-3 rounded-xl bg-zinc-900/40 border border-white/5 text-left group hover:border-white/20 transition-all flex items-center justify-between cursor-pointer">
<div class="min-w-0 flex-1">
<div class="min-w-0 flex-1"
x-data="{
expiresAt: '{{ $mailbox->expires_at?->toIso8601String() }}',
timeLeft: 'Never',
init() {
if (!this.expiresAt) return;
this.update();
setInterval(() => this.update(), 1000); // 1-sec interval
},
update() {
const end = new Date(this.expiresAt).getTime();
const now = new Date().getTime();
if (now > end) {
this.timeLeft = 'Expired';
return;
}
const diff = end - now;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const secs = Math.floor((diff % (1000 * 60)) / 1000);
if (days > 0) {
this.timeLeft = `${days}d ${hours}h ${mins}m ${secs}s`;
} else if (hours > 0) {
this.timeLeft = `${hours}h ${mins}m ${secs}s`;
} else if (mins > 0) {
this.timeLeft = `${mins}m ${secs}s`;
} else {
this.timeLeft = `${secs}s`;
}
}
}">
<div class="text-[10px] font-mono text-zinc-400 truncate group-hover:text-white transition-colors">{{ $mailbox->address }}</div>
<div class="text-[9px] text-zinc-600 font-bold uppercase mt-1">{{ $mailbox->expires_at?->diffForHumans(['short' => true]) ?? 'Never' }}</div>
<div class="text-[9px] text-zinc-600 font-bold uppercase mt-1" x-text="timeLeft"></div>
</div>
<svg class="w-4 h-4 text-zinc-700 group-hover:text-pink-500 translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>

View File

@@ -86,4 +86,39 @@ class UserTierTest extends TestCase
$this->assertTrue($accessibleDomains->contains($publicDomain));
$this->assertFalse($accessibleDomains->contains($premiumDomain));
}
public function test_mailbox_expiration_is_based_on_user_tier()
{
$domain = Domain::factory()->create(['allowed_types' => ['public'], 'name' => 'public.com']);
// Test Guest (1 day)
\Livewire\Livewire::test(\App\Livewire\Mailbox::class)
->call('autoCreateRandomMailbox');
$guestMailbox = \App\Models\Mailbox::latest()->first();
$this->assertEquals(1, round(now()->diffInDays($guestMailbox->expires_at)));
// Test Free (3 days)
$freeUser = User::factory()->free()->create();
$this->actingAs($freeUser);
\Livewire\Livewire::test(\App\Livewire\Mailbox::class)
->call('autoCreateRandomMailbox');
$freeMailbox = \App\Models\Mailbox::where('user_id', $freeUser->id)->first();
$this->assertEquals(3, round(now()->diffInDays($freeMailbox->expires_at)));
// Test Pro (7 days)
$proUser = User::factory()->pro()->create();
$this->actingAs($proUser);
\Livewire\Livewire::test(\App\Livewire\Mailbox::class)
->call('autoCreateRandomMailbox');
$proMailbox = \App\Models\Mailbox::where('user_id', $proUser->id)->first();
$this->assertEquals(7, round(now()->diffInDays($proMailbox->expires_at)));
// Test Enterprise (14 days)
$entUser = User::factory()->enterprise()->create();
$this->actingAs($entUser);
\Livewire\Livewire::test(\App\Livewire\Mailbox::class)
->call('autoCreateRandomMailbox');
$entMailbox = \App\Models\Mailbox::where('user_id', $entUser->id)->first();
$this->assertEquals(14, round(now()->diffInDays($entMailbox->expires_at)));
}
}