diff --git a/app/Filament/Pages/GenerateActivationKeys.php b/app/Filament/Pages/GenerateActivationKeys.php
new file mode 100644
index 0000000..f16841b
--- /dev/null
+++ b/app/Filament/Pages/GenerateActivationKeys.php
@@ -0,0 +1,154 @@
+form->fill();
+ }
+
+ protected function getFormSchema(): array
+ {
+ return [
+ Select::make('plan_id')
+ ->label('Select Plan')
+ ->options(Plan::all()->pluck('name', 'id'))
+ ->required(),
+
+ TextInput::make('quantity')
+ ->numeric()
+ ->minValue(1)
+ ->maxValue(100)
+ ->default(1)
+ ->required(),
+ ];
+ }
+
+ public function generate()
+ {
+ $data = $this->form->getState();
+ $plan = Plan::findOrFail($data['plan_id']);
+
+ for ($i = 0; $i < $data['quantity']; $i++) {
+ ActivationKey::create([
+ 'price_id' => $plan->pricing_id,
+ 'activation_key' => strtoupper('Z'.Str::random(16)),
+ 'is_activated' => false,
+ ]);
+ }
+
+ Notification::make()
+ ->title("{$data['quantity']} activation key(s) generated.")
+ ->success()
+ ->send();
+ $this->form->fill(); // Reset form
+ }
+
+ // === Table Setup ===
+ protected function getTableQuery(): \Illuminate\Database\Eloquent\Builder
+ {
+ return ActivationKey::query()->latest();
+ }
+
+ protected function getTableColumns(): array
+ {
+ return [
+ TextColumn::make('activation_key')
+ ->label('Key')
+ ->copyable(),
+
+ BooleanColumn::make('is_activated'),
+
+ TextColumn::make('user.email')
+ ->label('Activated By'),
+
+ TextColumn::make('billing_interval')
+ ->label('Interval')
+ ->getStateUsing(function ($record) {
+ $isMonthly = \App\Models\Plan::where('pricing_id', $record->price_id)->value('monthly_billing');
+ return $isMonthly ? 'Monthly' : 'Yearly';
+ }),
+
+
+ TextColumn::make('created_at')
+ ->dateTime(),
+ ];
+ }
+
+ protected function getTableFilters(): array
+ {
+ return [
+ SelectFilter::make('is_activated')
+ ->options([
+ true => 'Activated',
+ false => 'Not Activated',
+ ]),
+ SelectFilter::make('price_id')
+ ->label('Plan')
+ ->options(
+ Plan::pluck('name', 'pricing_id')
+ ),
+ ];
+ }
+
+ protected function getTableBulkActions(): array
+ {
+ return [
+ BulkAction::make('Download Keys')
+ ->action(fn (Collection $records) => $this->downloadKeys($records))
+ ->deselectRecordsAfterCompletion()
+ ->requiresConfirmation(),
+ ];
+ }
+
+ public function downloadKeys(Collection $records)
+ {
+ $text = $records->pluck('activation_key')->implode("\n");
+
+ $filename = 'activation_keys_' . now()->timestamp . '.txt';
+ // Store the file in the 'public' directory or a subdirectory within 'public'
+ $path = public_path("activation/{$filename}");
+
+ // Make sure the 'activation' folder exists, create it if it doesn't
+ if (!file_exists(public_path('activation'))) {
+ mkdir(public_path('activation'), 0777, true);
+ }
+
+ // Write the contents to the file
+ file_put_contents($path, $text);
+
+ // Return the response that allows users to download the file directly
+ return response()->download($path)->deleteFileAfterSend(true);
+ }
+
+}
diff --git a/app/Filament/Resources/PlanResource.php b/app/Filament/Resources/PlanResource.php
index 686309c..bd44d1a 100644
--- a/app/Filament/Resources/PlanResource.php
+++ b/app/Filament/Resources/PlanResource.php
@@ -45,12 +45,21 @@ class PlanResource extends Resource
TextInput::make('description'),
TextInput::make('product_id')->required(),
TextInput::make('pricing_id')->required(),
+ TextInput::make('shoppy_product_id')->nullable(),
TextInput::make('price')->numeric()->required(),
TextInput::make('mailbox_limit')->numeric()->required(),
Select::make('monthly_billing')->options([
1 => 'Monthly',
0 => 'Yearly',
- ])->default(1)->required(),
+ ])->required(),
+ Select::make('accept_stripe')->options([
+ 1 => 'Activate',
+ 0 => 'Disable',
+ ])->required(),
+ Select::make('accept_shoppy')->options([
+ 1 => 'Activate',
+ 0 => 'Disable',
+ ])->required(),
KeyValue::make('details')
->label('Plan Details (Optional)')
->keyPlaceholder('Name')
diff --git a/app/Livewire/Dashboard/Dashboard.php b/app/Livewire/Dashboard/Dashboard.php
index efc0dbf..271e6a3 100644
--- a/app/Livewire/Dashboard/Dashboard.php
+++ b/app/Livewire/Dashboard/Dashboard.php
@@ -35,7 +35,10 @@ class Dashboard extends Component
$user = auth()->user();
$userId = $user->id;
if ($user->subscribed()) {
- $subscription = $user->subscriptions()->where(['stripe_status' => 'active'])->orderByDesc('updated_at')->first();
+ $subscription = $user->subscriptions()
+ //->where(['stripe_status' => 'active'])
+ ->orderByDesc('updated_at')
+ ->first();
if ($subscription !== null) {
$subscriptionId = $subscription->stripe_id;
$cacheKey = "stripe_check_executed_user_{$userId}_{$subscriptionId}";
@@ -74,7 +77,7 @@ class Dashboard extends Component
]);
}
}
- Cache::put($cacheKey, true, now()->addHour());
+ Cache::put($cacheKey, true, now()->addMinute());
} catch (Exception $exception) {
\Log::error($exception->getMessage());
}
@@ -176,7 +179,7 @@ class Dashboard extends Component
$userPriceID = $result['items'][0]['stripe_price'];
$subscriptionEnd = $result['ends_at'];
- $planName = null; // Default value if not found
+ $planName = null;
foreach (config('app.plans') as $plan) {
if ($plan['pricing_id'] === $userPriceID) {
diff --git a/app/Livewire/Dashboard/Pricing.php b/app/Livewire/Dashboard/Pricing.php
index 80e0e43..d8674ca 100644
--- a/app/Livewire/Dashboard/Pricing.php
+++ b/app/Livewire/Dashboard/Pricing.php
@@ -2,11 +2,14 @@
namespace App\Livewire\Dashboard;
+use App\Models\ActivationKey;
+use App\Models\Plan;
use Livewire\Component;
class Pricing extends Component
{
public $plans;
+ public $activation_key;
public function mount(): void
{
@@ -18,6 +21,70 @@ class Pricing extends Component
$this->redirect(route('checkout', $pricing_id));
}
+ public function activateKey(): void
+ {
+ $this->validate([
+ 'activation_key' => 'required|alpha_num|max:30',
+ ], [
+ 'activation_key.required' => 'You must enter an activation key.',
+ 'activation_key.alpha_num' => 'The activation key may only contain letters and numbers (no special characters).',
+ 'activation_key.max' => 'The activation key must not exceed 30 characters.',
+ ]);
+
+ $trimmedKey = trim($this->activation_key);
+ $activation = ActivationKey::where('activation_key', $trimmedKey)
+ ->where('is_activated', false)
+ ->first();
+
+ if ($activation) {
+ if ($activation->price_id !== null) {
+ $result = $this->addSubscription($activation->price_id);
+ }
+ if ($result === true) {
+ $activation->is_activated = true;
+ $activation->user_id = auth()->id();
+ $activation->save();
+ session()->flash('success', 'Activation key is valid and has been activated. Refresh page to see changes.');
+ $this->reset('activation_key');
+ } else {
+ session()->flash('error', 'Something went wrong. Kindly drop a mail at contact@zemail.me to activate your subscription manually.');
+ }
+ } else {
+ session()->flash('error', 'Invalid or already activated key.');
+ }
+
+ }
+
+ private function addSubscription($price_id): bool
+ {
+ try {
+ $plan = Plan::where('pricing_id', $price_id)->firstOrFail();
+ $user = auth()->user();
+ $user->createOrGetStripeCustomer();
+ $user->updateStripeCustomer([
+ 'address' => [
+ 'postal_code' => '10001',
+ 'country' => 'US',
+ ],
+ 'name' => $user->name,
+ 'email' => $user->email,
+ ]);
+ $user->creditBalance($plan->price * 100, 'Premium Top-up for plan: ' . $plan->name);
+ $balance = $user->balance();
+ $user->newSubscription('default', $plan->pricing_id)->create();
+
+ if ($plan->monthly_billing == 1) {
+ $ends_at = now()->addMonth();
+ } else {
+ $ends_at = now()->addYear();
+ }
+ $user->subscription('default')->cancelAt($ends_at);
+ return true;
+ } catch (\Exception $e) {
+ \Log::error($e->getMessage());
+ return false;
+ }
+ }
public function render()
{
return view('livewire.dashboard.pricing');
diff --git a/app/Models/ActivationKey.php b/app/Models/ActivationKey.php
new file mode 100644
index 0000000..40bd91f
--- /dev/null
+++ b/app/Models/ActivationKey.php
@@ -0,0 +1,46 @@
+ 'boolean',
+ ];
+
+ /**
+ * Relationship: the user who redeemed the activation key (optional).
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ /**
+ * Scope to filter unactivated keys
+ */
+ public function scopeUnactivated($query)
+ {
+ return $query->where('is_activated', false);
+ }
+
+ /**
+ * Scope to filter activated keys
+ */
+ public function scopeActivated($query)
+ {
+ return $query->where('is_activated', true);
+ }
+}
diff --git a/app/Models/Plan.php b/app/Models/Plan.php
index d8af905..827630e 100644
--- a/app/Models/Plan.php
+++ b/app/Models/Plan.php
@@ -7,16 +7,20 @@ use Illuminate\Database\Eloquent\Model;
class Plan extends Model
{
protected $fillable = [
- 'name',
- 'description',
- 'product_id',
- 'pricing_id',
- 'price',
- 'mailbox_limit',
- 'monthly_billing',
- 'details'
+ 'name',
+ 'description',
+ 'product_id',
+ 'pricing_id',
+ 'shoppy_product_id',
+ 'accept_stripe',
+ 'accept_shoppy',
+ 'price',
+ 'mailbox_limit',
+ 'monthly_billing',
+ 'details',
];
+
protected $casts = [
'details' => 'json',
'monthly_billing' => 'boolean',
diff --git a/database/migrations/2025_05_16_015550_create_activation_keys_table.php b/database/migrations/2025_05_16_015550_create_activation_keys_table.php
new file mode 100644
index 0000000..fc0da9c
--- /dev/null
+++ b/database/migrations/2025_05_16_015550_create_activation_keys_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
+ $table->string('activation_key')->unique();
+ $table->string('price_id')->collation('utf8_bin');
+ $table->boolean('is_activated')->default(false);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('activation_keys');
+ }
+};
diff --git a/database/migrations/2025_05_16_024757_add_shoppy_and_accept_columns_to_plans_table.php b/database/migrations/2025_05_16_024757_add_shoppy_and_accept_columns_to_plans_table.php
new file mode 100644
index 0000000..caa7211
--- /dev/null
+++ b/database/migrations/2025_05_16_024757_add_shoppy_and_accept_columns_to_plans_table.php
@@ -0,0 +1,30 @@
+string('shoppy_product_id')->nullable()->after('pricing_id');
+ $table->boolean('accept_stripe')->default(false)->after('shoppy_product_id');
+ $table->boolean('accept_shoppy')->default(false)->after('accept_stripe');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('plans', function (Blueprint $table) {
+ $table->dropColumn(['shoppy_product_id', 'accept_stripe', 'accept_shoppy']);
+ });
+ }
+};
diff --git a/database/seeders/UpdatePlansTableSeeder.php b/database/seeders/UpdatePlansTableSeeder.php
new file mode 100644
index 0000000..c39396c
--- /dev/null
+++ b/database/seeders/UpdatePlansTableSeeder.php
@@ -0,0 +1,28 @@
+update([
+ 'shoppy_product_id' => 'MsYfrRX',
+ 'accept_stripe' => 1,
+ 'accept_shoppy' => 1,
+ ]);
+
+ Plan::where('id', 2)->update([
+ 'shoppy_product_id' => '1oU5SNT',
+ 'accept_stripe' => 1,
+ 'accept_shoppy' => 1,
+ ]);
+ }
+}
diff --git a/resources/views/filament/pages/generate-activation-keys.blade.php b/resources/views/filament/pages/generate-activation-keys.blade.php
new file mode 100644
index 0000000..16159b4
--- /dev/null
+++ b/resources/views/filament/pages/generate-activation-keys.blade.php
@@ -0,0 +1,10 @@
+