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 @@ + +
+ {{ $this->form }} + + + Generate Activation Keys + +
+ {{ $this->table }} +
diff --git a/resources/views/livewire/dashboard/pricing.blade.php b/resources/views/livewire/dashboard/pricing.blade.php index 5513748..0fc7e5e 100644 --- a/resources/views/livewire/dashboard/pricing.blade.php +++ b/resources/views/livewire/dashboard/pricing.blade.php @@ -1,4 +1,5 @@
+

Purchase Subscription

@@ -35,12 +36,70 @@ @endif + @if($plan->accept_stripe && $plan->pricing_id !== null) - Choose Plan + Pay with card + @endif + @if($plan->accept_shoppy && $plan->shoppy_product_id !== null) + + Pay with crypto + + @endif
@endforeach @endif + + + +
+ +
+

Have an Activation Key?

+
+
+ + +
+
+ Redeem your activation key, purchased with Pay with Crypto option. +
+
+ @error('activation_key') +
+ {{ $message }} +
+ @enderror + + @if (session()->has('success')) +
+ {{ session('success') }} +
+ @endif + + @if (session()->has('error')) +
+ {{ session('error') }} +
+ @endif +
+
+