added crypto pm and activation key system

This commit is contained in:
Gitea
2025-05-16 11:24:08 +05:30
parent 23b5a45d0b
commit 93515e7845
11 changed files with 454 additions and 13 deletions

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Filament\Pages;
use App\Models\ActivationKey;
use App\Models\Plan;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Columns\BooleanColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Support\Collection;
use Response;
use Str;
use Filament\Notifications\Notification;
class GenerateActivationKeys extends Page implements HasForms, HasTable
{
use InteractsWithForms, InteractsWithTable;
protected static ?string $navigationIcon = 'heroicon-o-key';
protected static string $view = 'filament.pages.generate-activation-keys';
protected static ?string $navigationGroup = 'Admin';
protected static ?string $title = 'Activation Keys';
public $plan_id;
public $quantity = 1;
public function mount(): void
{
$this->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);
}
}

View File

@@ -45,12 +45,21 @@ class PlanResource extends Resource
TextInput::make('description'), TextInput::make('description'),
TextInput::make('product_id')->required(), TextInput::make('product_id')->required(),
TextInput::make('pricing_id')->required(), TextInput::make('pricing_id')->required(),
TextInput::make('shoppy_product_id')->nullable(),
TextInput::make('price')->numeric()->required(), TextInput::make('price')->numeric()->required(),
TextInput::make('mailbox_limit')->numeric()->required(), TextInput::make('mailbox_limit')->numeric()->required(),
Select::make('monthly_billing')->options([ Select::make('monthly_billing')->options([
1 => 'Monthly', 1 => 'Monthly',
0 => 'Yearly', 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') KeyValue::make('details')
->label('Plan Details (Optional)') ->label('Plan Details (Optional)')
->keyPlaceholder('Name') ->keyPlaceholder('Name')

View File

@@ -35,7 +35,10 @@ class Dashboard extends Component
$user = auth()->user(); $user = auth()->user();
$userId = $user->id; $userId = $user->id;
if ($user->subscribed()) { 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) { if ($subscription !== null) {
$subscriptionId = $subscription->stripe_id; $subscriptionId = $subscription->stripe_id;
$cacheKey = "stripe_check_executed_user_{$userId}_{$subscriptionId}"; $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) { } catch (Exception $exception) {
\Log::error($exception->getMessage()); \Log::error($exception->getMessage());
} }
@@ -176,7 +179,7 @@ class Dashboard extends Component
$userPriceID = $result['items'][0]['stripe_price']; $userPriceID = $result['items'][0]['stripe_price'];
$subscriptionEnd = $result['ends_at']; $subscriptionEnd = $result['ends_at'];
$planName = null; // Default value if not found $planName = null;
foreach (config('app.plans') as $plan) { foreach (config('app.plans') as $plan) {
if ($plan['pricing_id'] === $userPriceID) { if ($plan['pricing_id'] === $userPriceID) {

View File

@@ -2,11 +2,14 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use App\Models\ActivationKey;
use App\Models\Plan;
use Livewire\Component; use Livewire\Component;
class Pricing extends Component class Pricing extends Component
{ {
public $plans; public $plans;
public $activation_key;
public function mount(): void public function mount(): void
{ {
@@ -18,6 +21,70 @@ class Pricing extends Component
$this->redirect(route('checkout', $pricing_id)); $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() public function render()
{ {
return view('livewire.dashboard.pricing'); return view('livewire.dashboard.pricing');

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ActivationKey extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'activation_key',
'price_id',
'is_activated',
];
protected $casts = [
'is_activated' => '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);
}
}

View File

@@ -11,12 +11,16 @@ class Plan extends Model
'description', 'description',
'product_id', 'product_id',
'pricing_id', 'pricing_id',
'shoppy_product_id',
'accept_stripe',
'accept_shoppy',
'price', 'price',
'mailbox_limit', 'mailbox_limit',
'monthly_billing', 'monthly_billing',
'details' 'details',
]; ];
protected $casts = [ protected $casts = [
'details' => 'json', 'details' => 'json',
'monthly_billing' => 'boolean', 'monthly_billing' => 'boolean',

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('activation_keys', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('plans', function (Blueprint $table) {
$table->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']);
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
namespace Database\Seeders;
use App\Models\Plan;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class UpdatePlansTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Plan::where('id', 1)->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,
]);
}
}

View File

@@ -0,0 +1,10 @@
<x-filament::page>
<form wire:submit.prevent="generate" class="space-y-6">
{{ $this->form }}
<x-filament::button type="submit">
Generate Activation Keys
</x-filament::button>
</form>
{{ $this->table }}
</x-filament::page>

View File

@@ -1,4 +1,5 @@
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-12 lg:px-8 "> <div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-12 lg:px-8 ">
<script src="https://shoppy.gg/api/embed.js"></script>
<div class="w-full mb-8 items-center flex justify-center"> <div class="w-full mb-8 items-center flex justify-center">
<h1 class="text-center text-3xl text-gray-900 dark:text-gray-200">Purchase Subscription</h1> <h1 class="text-center text-3xl text-gray-900 dark:text-gray-200">Purchase Subscription</h1>
</div> </div>
@@ -35,12 +36,70 @@
@endif @endif
</ul> </ul>
@if($plan->accept_stripe && $plan->pricing_id !== null)
<flux:button variant="primary" class="w-full mt-6 cursor-pointer" wire:click="choosePlan('{{ $plan->pricing_id }}')"> <flux:button variant="primary" class="w-full mt-6 cursor-pointer" wire:click="choosePlan('{{ $plan->pricing_id }}')">
Choose Plan Pay with card
</flux:button> </flux:button>
@endif
@if($plan->accept_shoppy && $plan->shoppy_product_id !== null)
<flux:button variant="filled" class="w-full mt-2 cursor-pointer" data-shoppy-product="{{ $plan->shoppy_product_id }}">
Pay with crypto
</flux:button>
@endif
</div> </div>
@endforeach @endforeach
@endif @endif
</div> </div>
<div class="w-full mt-8">
<flux:separator text="or" />
<div class="w-full mt-4 mb-8 items-center flex justify-center">
<h1 class="text-center text-3xl text-gray-900 dark:text-gray-200">Have an Activation Key?</h1>
</div>
<div class="flex rounded-lg overflow-hidden border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-zinc-800">
<input
type="text"
wire:model="activation_key"
class="w-full px-4 py-2 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 placeholder-zinc-500 focus:outline-none"
placeholder="Enter your activation key"
/>
<button
wire:click="activateKey"
class="cursor-pointer px-5 text-white transition-colors dark:text-white bg-[#4361EE] dark:bg-[#4361EE] disabled:bg-gray-400 disabled:cursor-not-allowed"
:disabled="wire:loading"
>
<!-- Show Loader when loading -->
<span wire:loading.remove>Activate</span>
<span wire:loading class="flex justify-center items-center px-4">
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_7NYg{animation:spinner_0KQs 1.2s cubic-bezier(0.52,.6,.25,.99) infinite}@keyframes spinner_0KQs{0%{r:0;opacity:1}100%{r:11px;opacity:0}}</style><circle class="spinner_7NYg" cx="12" cy="12" r="0" fill="white"/></svg>
</span>
</button>
</div>
<div class="mt-2 text-center text-sm text-gray-500 dark:text-gray-400">
Redeem your activation key, purchased with Pay with Crypto option.
</div>
<div class="mt-3">
@error('activation_key')
<div class="mt-4 app-primary">
{{ $message }}
</div>
@enderror
<!-- Success/Error Message -->
@if (session()->has('success'))
<div class="mt-4" style="color: #00AB55">
{{ session('success') }}
</div>
@endif
@if (session()->has('error'))
<div class="mt-4 text-green-700">
{{ session('error') }}
</div>
@endif
</div>
</div>
</div> </div>