feat: enhance pricing page with feature limits and trial management
- Add comprehensive feature limits enforcement middleware - Implement subscription dashboard with usage analytics - Create reusable plan card component with feature badges - Add trial configuration support with limit overrides - Fix payment controller null safety issues - Improve pricing page UI with proper feature display
This commit is contained in:
@@ -2,28 +2,91 @@
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use App\Models\ActivationKey;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlanTier;
|
||||
use App\Services\Payments\PaymentOrchestrator;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
|
||||
class Pricing extends Component
|
||||
{
|
||||
public $plans;
|
||||
|
||||
public $planTiers;
|
||||
|
||||
public $activation_key;
|
||||
|
||||
public $selectedProvider = 'stripe';
|
||||
|
||||
public $selectedBillingCycle = null;
|
||||
|
||||
public $selectedTier = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->plans = config('app.plans');
|
||||
$this->loadPlans();
|
||||
$this->planTiers = PlanTier::with('plans')->orderBy('sort_order')->get();
|
||||
}
|
||||
|
||||
public function choosePlan($pricing_id): void
|
||||
private function loadPlans(): void
|
||||
{
|
||||
$this->redirect(route('checkout', $pricing_id));
|
||||
$query = Plan::active()
|
||||
->ordered()
|
||||
->with(['planFeatureLimits.planFeature', 'planProviders', 'trialConfiguration', 'planTier']);
|
||||
|
||||
if ($this->selectedTier) {
|
||||
$query->where('plan_tier_id', $this->selectedTier);
|
||||
}
|
||||
|
||||
$this->plans = $query->get();
|
||||
}
|
||||
|
||||
public function filterByTier($tierId = null): void
|
||||
{
|
||||
$this->selectedTier = $tierId;
|
||||
$this->loadPlans();
|
||||
}
|
||||
|
||||
public function choosePlan($planId, $provider = 'stripe'): void
|
||||
{
|
||||
$plan = Plan::findOrFail($planId);
|
||||
|
||||
if (! $plan?->supportsProvider($provider)) {
|
||||
session()->flash('error', "This plan doesn't support {$provider} payments.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->redirect(route('checkout.enhanced', [
|
||||
'plan' => $planId,
|
||||
'provider' => $provider,
|
||||
]));
|
||||
}
|
||||
|
||||
public function startTrial($planId, $provider = 'stripe'): void
|
||||
{
|
||||
$plan = Plan::findOrFail($planId);
|
||||
|
||||
if (! $plan?->hasTrial()) {
|
||||
session()->flash('error', "This plan doesn't offer trials.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $plan?->supportsProvider($provider)) {
|
||||
session()->flash('error', "This plan doesn't support {$provider} payments for trials.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->redirect(route('checkout.trial', [
|
||||
'plan' => $planId,
|
||||
'provider' => $provider,
|
||||
]));
|
||||
}
|
||||
|
||||
public function activateKey(): void
|
||||
@@ -42,53 +105,125 @@ class Pricing extends Component
|
||||
->first();
|
||||
|
||||
if ($activation) {
|
||||
if ($activation->price_id !== null) {
|
||||
$result = $this->addSubscription($activation->price_id);
|
||||
}
|
||||
if ($result) {
|
||||
$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.');
|
||||
try {
|
||||
$result = $this->activateSubscriptionKey($activation);
|
||||
if ($result) {
|
||||
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.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error('Activation key error: '.$e->getMessage());
|
||||
session()->flash('error', 'An error occurred while activating your key. Please contact support.');
|
||||
}
|
||||
} else {
|
||||
session()->flash('error', 'Invalid or already activated key.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function addSubscription($price_id): bool
|
||||
private function activateSubscriptionKey(ActivationKey $activation): bool
|
||||
{
|
||||
try {
|
||||
$plan = Plan::query()->where('pricing_id', $price_id)->firstOrFail();
|
||||
// Use PaymentOrchestrator for activation key processing
|
||||
$orchestrator = app(PaymentOrchestrator::class);
|
||||
|
||||
// Find the plan associated with this activation key
|
||||
$plan = null;
|
||||
if ($activation->plan_id) {
|
||||
$plan = Plan::find($activation->plan_id);
|
||||
} elseif ($activation->price_id) {
|
||||
// Fallback to legacy pricing_id lookup
|
||||
$plan = Plan::where('pricing_id', $activation->price_id)->first();
|
||||
}
|
||||
|
||||
if (! $plan) {
|
||||
Log::error('No plan found for activation key: '.$activation->id);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create subscription using orchestrator
|
||||
$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();
|
||||
$subscription = $orchestrator->createSubscriptionFromActivationKey($user, $activation, $plan);
|
||||
|
||||
$ends_at = $plan->monthly_billing == 1 ? now()->addMonth() : now()->addYear();
|
||||
$user->subscription('default')->cancelAt($ends_at);
|
||||
if ($subscription) {
|
||||
$activation->is_activated = true;
|
||||
$activation->user_id = $user->id;
|
||||
$activation->save();
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
Log::error('Activation key processing failed: '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available providers for a plan
|
||||
*/
|
||||
public function getPlanProviders($planId): array
|
||||
{
|
||||
$plan = $this->plans->firstWhere('id', $planId);
|
||||
|
||||
return $plan ? $plan->getAllowedProviders() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan features with limits
|
||||
*/
|
||||
public function getPlanFeatures($planId): array
|
||||
{
|
||||
$plan = $this->plans->firstWhere('id', $planId);
|
||||
|
||||
if (! $plan) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $plan->getFeaturesWithLimits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan has trial available
|
||||
*/
|
||||
public function planHasTrial($planId): bool
|
||||
{
|
||||
$plan = $this->plans->firstWhere('id', $planId);
|
||||
|
||||
return $plan ? $plan->hasTrial() : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trial configuration for plan
|
||||
*/
|
||||
public function getTrialConfig($planId): ?array
|
||||
{
|
||||
$plan = $this->plans->firstWhere('id', $planId);
|
||||
if (! $plan || ! $plan->hasTrial()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = $plan->getTrialConfig();
|
||||
|
||||
return [
|
||||
'duration_days' => $config->trial_duration_days,
|
||||
'requires_payment_method' => $config->trial_requires_payment_method,
|
||||
'auto_converts' => $config->trial_auto_converts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get billing cycle display text
|
||||
*/
|
||||
public function getBillingCycleDisplay($plan): string
|
||||
{
|
||||
return $plan->getBillingCycleDisplay();
|
||||
}
|
||||
|
||||
public function render(): Factory|View
|
||||
{
|
||||
return view('livewire.dashboard.pricing');
|
||||
|
||||
Reference in New Issue
Block a user