feat(mailbox): Implement tier-based dynamic expiration with real-time Alpine.js countdown
This commit is contained in:
@@ -223,7 +223,7 @@ class Mailbox extends Component
|
|||||||
'created_ip' => request()->ip(),
|
'created_ip' => request()->ip(),
|
||||||
'last_accessed_ip' => request()->ip(),
|
'last_accessed_ip' => request()->ip(),
|
||||||
'last_accessed_at' => now(),
|
'last_accessed_at' => now(),
|
||||||
'expires_at' => now()->addDays(7), // Default expiry
|
'expires_at' => now()->addDays($this->getValidityDays()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->currentMailboxId = $mailbox->id;
|
$this->currentMailboxId = $mailbox->id;
|
||||||
@@ -262,7 +262,7 @@ class Mailbox extends Component
|
|||||||
'created_ip' => request()->ip(),
|
'created_ip' => request()->ip(),
|
||||||
'last_accessed_ip' => request()->ip(),
|
'last_accessed_ip' => request()->ip(),
|
||||||
'last_accessed_at' => now(),
|
'last_accessed_at' => now(),
|
||||||
'expires_at' => now()->addDays(7),
|
'expires_at' => now()->addDays($this->getValidityDays()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->currentMailboxId = $mailbox->id;
|
$this->currentMailboxId = $mailbox->id;
|
||||||
@@ -270,6 +270,27 @@ class Mailbox extends Component
|
|||||||
Session::put('current_mailbox_id', $mailbox->id);
|
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
|
public function finishAutoCreation(): void
|
||||||
{
|
{
|
||||||
if ($this->getActiveMailboxesProperty()->isEmpty()) {
|
if ($this->getActiveMailboxesProperty()->isEmpty()) {
|
||||||
|
|||||||
@@ -154,18 +154,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] font-mono text-white break-all mb-4">{{ $currentMailbox->address }}</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]">
|
<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-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>
|
||||||
<div class="h-1 bg-white/5 rounded-full overflow-hidden">
|
<div class="h-1 bg-white/5 rounded-full overflow-hidden">
|
||||||
@php
|
<div class="h-full bg-gradient-to-r from-pink-500 to-emerald-500 transition-all duration-1000 ease-linear" :style="`width: ${percent}%`"></div>
|
||||||
$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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,9 +231,43 @@
|
|||||||
@if($mailbox->id !== $currentMailboxId)
|
@if($mailbox->id !== $currentMailboxId)
|
||||||
<button wire:click="switchMailbox({{ $mailbox->id }})"
|
<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">
|
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-[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>
|
</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>
|
<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>
|
</button>
|
||||||
|
|||||||
@@ -86,4 +86,39 @@ class UserTierTest extends TestCase
|
|||||||
$this->assertTrue($accessibleDomains->contains($publicDomain));
|
$this->assertTrue($accessibleDomains->contains($publicDomain));
|
||||||
$this->assertFalse($accessibleDomains->contains($premiumDomain));
|
$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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user