From 1d3d6287def4b86b112144b769e8c67c705ec9fe Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 5 Dec 2025 10:41:29 +0000 Subject: [PATCH 01/34] wip --- app/Enums/PluginActivityType.php | 45 ++ app/Enums/PluginStatus.php | 28 + app/Enums/PluginType.php | 17 + app/Filament/Resources/PluginResource.php | 255 +++++++ .../PluginResource/Pages/ListPlugins.php | 19 + .../PluginResource/Pages/ViewPlugin.php | 11 + .../ActivitiesRelationManager.php | 55 ++ .../Controllers/CustomerPluginController.php | 93 +++ .../Controllers/PluginDirectoryController.php | 31 + app/Http/Requests/SubmitPluginRequest.php | 47 ++ .../UpdatePluginDescriptionRequest.php | 34 + app/Livewire/PluginDirectory.php | 51 ++ app/Models/Plugin.php | 229 ++++++ app/Models/PluginActivity.php | 35 + app/Models/User.php | 8 + app/Notifications/PluginApproved.php | 54 ++ app/Notifications/PluginRejected.php | 57 ++ database/factories/PluginFactory.php | 186 +++++ ...2025_12_03_124822_create_plugins_table.php | 35 + ..._and_rejection_reason_to_plugins_table.php | 29 + ..._154716_create_plugin_activities_table.php | 35 + ...3_161416_add_featured_to_plugins_table.php | 30 + ...75340_add_description_to_plugins_table.php | 28 + database/seeders/PluginSeeder.php | 127 ++++ .../views/components/icons/puzzle.blade.php | 12 + .../navbar/device-dropdowns.blade.php | 7 + .../views/components/plugin-card.blade.php | 55 ++ .../views/customer/licenses/index.blade.php | 4 + .../views/customer/plugins/create.blade.php | 220 ++++++ .../views/customer/plugins/index.blade.php | 189 +++++ .../views/customer/plugins/show.blade.php | 135 ++++ .../views/livewire/plugin-directory.blade.php | 117 ++++ resources/views/plugins.blade.php | 658 ++++++++++++++++++ resources/views/pricing.blade.php | 11 + routes/web.php | 15 +- 35 files changed, 2960 insertions(+), 2 deletions(-) create mode 100644 app/Enums/PluginActivityType.php create mode 100644 app/Enums/PluginStatus.php create mode 100644 app/Enums/PluginType.php create mode 100644 app/Filament/Resources/PluginResource.php create mode 100644 app/Filament/Resources/PluginResource/Pages/ListPlugins.php create mode 100644 app/Filament/Resources/PluginResource/Pages/ViewPlugin.php create mode 100644 app/Filament/Resources/PluginResource/RelationManagers/ActivitiesRelationManager.php create mode 100644 app/Http/Controllers/CustomerPluginController.php create mode 100644 app/Http/Controllers/PluginDirectoryController.php create mode 100644 app/Http/Requests/SubmitPluginRequest.php create mode 100644 app/Http/Requests/UpdatePluginDescriptionRequest.php create mode 100644 app/Livewire/PluginDirectory.php create mode 100644 app/Models/Plugin.php create mode 100644 app/Models/PluginActivity.php create mode 100644 app/Notifications/PluginApproved.php create mode 100644 app/Notifications/PluginRejected.php create mode 100644 database/factories/PluginFactory.php create mode 100644 database/migrations/2025_12_03_124822_create_plugins_table.php create mode 100644 database/migrations/2025_12_03_141900_add_anystack_id_and_rejection_reason_to_plugins_table.php create mode 100644 database/migrations/2025_12_03_154716_create_plugin_activities_table.php create mode 100644 database/migrations/2025_12_03_161416_add_featured_to_plugins_table.php create mode 100644 database/migrations/2025_12_03_175340_add_description_to_plugins_table.php create mode 100644 database/seeders/PluginSeeder.php create mode 100644 resources/views/components/icons/puzzle.blade.php create mode 100644 resources/views/components/plugin-card.blade.php create mode 100644 resources/views/customer/plugins/create.blade.php create mode 100644 resources/views/customer/plugins/index.blade.php create mode 100644 resources/views/customer/plugins/show.blade.php create mode 100644 resources/views/livewire/plugin-directory.blade.php create mode 100644 resources/views/plugins.blade.php diff --git a/app/Enums/PluginActivityType.php b/app/Enums/PluginActivityType.php new file mode 100644 index 00000000..56a26126 --- /dev/null +++ b/app/Enums/PluginActivityType.php @@ -0,0 +1,45 @@ + 'Submitted', + self::Resubmitted => 'Resubmitted', + self::Approved => 'Approved', + self::Rejected => 'Rejected', + self::DescriptionUpdated => 'Description Updated', + }; + } + + public function color(): string + { + return match ($this) { + self::Submitted => 'info', + self::Resubmitted => 'info', + self::Approved => 'success', + self::Rejected => 'danger', + self::DescriptionUpdated => 'gray', + }; + } + + public function icon(): string + { + return match ($this) { + self::Submitted => 'heroicon-o-paper-airplane', + self::Resubmitted => 'heroicon-o-arrow-path', + self::Approved => 'heroicon-o-check-circle', + self::Rejected => 'heroicon-o-x-circle', + self::DescriptionUpdated => 'heroicon-o-pencil-square', + }; + } +} diff --git a/app/Enums/PluginStatus.php b/app/Enums/PluginStatus.php new file mode 100644 index 00000000..4fd44020 --- /dev/null +++ b/app/Enums/PluginStatus.php @@ -0,0 +1,28 @@ + 'Pending Review', + self::Approved => 'Approved', + self::Rejected => 'Rejected', + }; + } + + public function color(): string + { + return match ($this) { + self::Pending => 'yellow', + self::Approved => 'green', + self::Rejected => 'red', + }; + } +} diff --git a/app/Enums/PluginType.php b/app/Enums/PluginType.php new file mode 100644 index 00000000..27df3858 --- /dev/null +++ b/app/Enums/PluginType.php @@ -0,0 +1,17 @@ + 'Free', + self::Paid => 'Paid', + }; + } +} diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php new file mode 100644 index 00000000..abc88ccb --- /dev/null +++ b/app/Filament/Resources/PluginResource.php @@ -0,0 +1,255 @@ +schema([ + Forms\Components\Section::make('Plugin Details') + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Composer Package Name') + ->disabled(), + + Forms\Components\Select::make('type') + ->options(PluginType::class) + ->disabled(), + + Forms\Components\TextInput::make('anystack_id') + ->label('Anystack Product ID') + ->disabled() + ->visible(fn (Plugin $record) => $record->isPaid()), + + Forms\Components\Select::make('status') + ->options(PluginStatus::class) + ->disabled(), + + Forms\Components\Textarea::make('description') + ->label('Description') + ->disabled() + ->columnSpanFull(), + + Forms\Components\Textarea::make('rejection_reason') + ->label('Rejection Reason') + ->disabled() + ->visible(fn (Plugin $record) => $record->isRejected()), + ]) + ->columns(2), + + Forms\Components\Section::make('Submission Info') + ->schema([ + Forms\Components\Select::make('user_id') + ->relationship('user', 'email') + ->disabled(), + + Forms\Components\DateTimePicker::make('created_at') + ->label('Submitted At') + ->disabled(), + + Forms\Components\Select::make('approved_by') + ->relationship('approvedBy', 'email') + ->disabled() + ->visible(fn (Plugin $record) => $record->approved_by !== null), + + Forms\Components\DateTimePicker::make('approved_at') + ->disabled() + ->visible(fn (Plugin $record) => $record->approved_at !== null), + ]) + ->columns(2), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label('Package Name') + ->searchable() + ->sortable() + ->copyable() + ->fontFamily('mono'), + + Tables\Columns\TextColumn::make('type') + ->badge() + ->color(fn (PluginType $state): string => match ($state) { + PluginType::Free => 'gray', + PluginType::Paid => 'success', + }) + ->sortable(), + + Tables\Columns\TextColumn::make('user.email') + ->label('Submitted By') + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('status') + ->badge() + ->color(fn (PluginStatus $state): string => match ($state) { + PluginStatus::Pending => 'warning', + PluginStatus::Approved => 'success', + PluginStatus::Rejected => 'danger', + }) + ->sortable(), + + Tables\Columns\ToggleColumn::make('featured') + ->sortable(), + + Tables\Columns\TextColumn::make('created_at') + ->label('Submitted') + ->dateTime() + ->sortable(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('status') + ->options(PluginStatus::class), + Tables\Filters\SelectFilter::make('type') + ->options(PluginType::class), + Tables\Filters\TernaryFilter::make('featured'), + ]) + ->actions([ + // Approve Action + Tables\Actions\Action::make('approve') + ->icon('heroicon-o-check') + ->color('success') + ->visible(fn (Plugin $record) => $record->isPending()) + ->action(fn (Plugin $record) => $record->approve(auth()->id())) + ->requiresConfirmation() + ->modalHeading('Approve Plugin') + ->modalDescription(fn (Plugin $record) => "Are you sure you want to approve '{$record->name}'?"), + + // Reject Action + Tables\Actions\Action::make('reject') + ->icon('heroicon-o-x-mark') + ->color('danger') + ->visible(fn (Plugin $record) => $record->isPending() || $record->isApproved()) + ->form([ + Forms\Components\Textarea::make('rejection_reason') + ->label('Reason for Rejection') + ->required() + ->rows(3) + ->placeholder('Please explain why this plugin is being rejected...'), + ]) + ->action(fn (Plugin $record, array $data) => $record->reject($data['rejection_reason'], auth()->id())) + ->modalHeading('Reject Plugin') + ->modalDescription(fn (Plugin $record) => "Are you sure you want to reject '{$record->name}'?"), + + // External Links Group + Tables\Actions\ActionGroup::make([ + // Packagist Link (Free plugins only) + Tables\Actions\Action::make('viewPackagist') + ->label('View on Packagist') + ->icon('heroicon-o-arrow-top-right-on-square') + ->color('gray') + ->url(fn (Plugin $record) => $record->getPackagistUrl()) + ->openUrlInNewTab() + ->visible(fn (Plugin $record) => $record->isFree()), + + // GitHub Link (Free plugins only) + Tables\Actions\Action::make('viewGithub') + ->label('View on GitHub') + ->icon('heroicon-o-arrow-top-right-on-square') + ->color('gray') + ->url(fn (Plugin $record) => $record->getGithubUrl()) + ->openUrlInNewTab() + ->visible(fn (Plugin $record) => $record->isFree()), + + // Anystack Link (Paid plugins only) + Tables\Actions\Action::make('viewAnystack') + ->label('View on Anystack') + ->icon('heroicon-o-arrow-top-right-on-square') + ->color('gray') + ->url(fn (Plugin $record) => $record->getAnystackUrl()) + ->openUrlInNewTab() + ->visible(fn (Plugin $record) => $record->isPaid() && $record->anystack_id), + + // Anystack Instructions (Paid plugins without ID) + Tables\Actions\Action::make('anystackInstructions') + ->label('Anystack Setup') + ->icon('heroicon-o-information-circle') + ->color('warning') + ->visible(fn (Plugin $record) => $record->isPaid()) + ->modalHeading('Anystack Verification Required') + ->modalDescription('For paid plugins, verify that the developer has applied to the "NativePHP Plugin Directory" affiliate program in their Anystack dashboard under the Advertising section.') + ->modalSubmitAction(false) + ->modalCancelActionLabel('Close'), + + // Edit Description Action + Tables\Actions\Action::make('editDescription') + ->label('Edit Description') + ->icon('heroicon-o-pencil-square') + ->color('gray') + ->form([ + Forms\Components\Textarea::make('description') + ->label('Description') + ->required() + ->rows(5) + ->maxLength(1000) + ->default(fn (Plugin $record) => $record->description) + ->placeholder('Describe what this plugin does...'), + ]) + ->action(fn (Plugin $record, array $data) => $record->updateDescription($data['description'], auth()->id())) + ->modalHeading('Edit Plugin Description') + ->modalDescription(fn (Plugin $record) => "Update the description for '{$record->name}'"), + + Tables\Actions\ViewAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->label('More') + ->icon('heroicon-m-ellipsis-vertical'), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\BulkAction::make('approve') + ->icon('heroicon-o-check') + ->color('success') + ->action(function ($records) { + $records->each(fn (Plugin $record) => $record->approve(auth()->id())); + }) + ->requiresConfirmation() + ->modalHeading('Approve Selected Plugins') + ->modalDescription('Are you sure you want to approve all selected plugins?'), + + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } + + public static function getRelations(): array + { + return [ + RelationManagers\ActivitiesRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPlugins::route('/'), + 'view' => Pages\ViewPlugin::route('/{record}'), + ]; + } +} diff --git a/app/Filament/Resources/PluginResource/Pages/ListPlugins.php b/app/Filament/Resources/PluginResource/Pages/ListPlugins.php new file mode 100644 index 00000000..ba97676c --- /dev/null +++ b/app/Filament/Resources/PluginResource/Pages/ListPlugins.php @@ -0,0 +1,19 @@ +columns([ + Tables\Columns\TextColumn::make('type') + ->badge() + ->color(fn (PluginActivityType $state): string => $state->color()) + ->icon(fn (PluginActivityType $state): string => $state->icon()) + ->sortable(), + + Tables\Columns\TextColumn::make('from_status') + ->label('From') + ->badge() + ->color('gray') + ->placeholder('-'), + + Tables\Columns\TextColumn::make('to_status') + ->label('To') + ->badge() + ->color('gray'), + + Tables\Columns\TextColumn::make('note') + ->label('Note/Reason') + ->limit(50) + ->tooltip(fn ($record) => $record->note) + ->placeholder('-'), + + Tables\Columns\TextColumn::make('causer.email') + ->label('By') + ->placeholder('System'), + + Tables\Columns\TextColumn::make('created_at') + ->label('Date') + ->dateTime() + ->sortable(), + ]) + ->defaultSort('created_at', 'desc') + ->paginated([10, 25, 50]); + } +} diff --git a/app/Http/Controllers/CustomerPluginController.php b/app/Http/Controllers/CustomerPluginController.php new file mode 100644 index 00000000..9e294b56 --- /dev/null +++ b/app/Http/Controllers/CustomerPluginController.php @@ -0,0 +1,93 @@ +middleware('auth'); + } + + public function index(): View + { + $user = Auth::user(); + $plugins = $user->plugins()->orderBy('created_at', 'desc')->get(); + + return view('customer.plugins.index', compact('plugins')); + } + + public function create(): View + { + return view('customer.plugins.create'); + } + + public function store(SubmitPluginRequest $request): RedirectResponse + { + $user = Auth::user(); + + $user->plugins()->create([ + 'name' => $request->name, + 'type' => $request->type, + 'anystack_id' => $request->anystack_id, + 'status' => PluginStatus::Pending, + ]); + + return redirect()->route('customer.plugins.index') + ->with('success', 'Your plugin has been submitted for review!'); + } + + public function show(Plugin $plugin): View + { + $user = Auth::user(); + + if ($plugin->user_id !== $user->id) { + abort(403); + } + + return view('customer.plugins.show', compact('plugin')); + } + + public function update(UpdatePluginDescriptionRequest $request, Plugin $plugin): RedirectResponse + { + $user = Auth::user(); + + if ($plugin->user_id !== $user->id) { + abort(403); + } + + $plugin->updateDescription($request->description, $user->id); + + return redirect()->route('customer.plugins.show', $plugin) + ->with('success', 'Plugin description updated successfully!'); + } + + public function resubmit(Plugin $plugin): RedirectResponse + { + $user = Auth::user(); + + // Ensure the plugin belongs to the current user + if ($plugin->user_id !== $user->id) { + abort(403); + } + + // Only rejected plugins can be resubmitted + if (! $plugin->isRejected()) { + return redirect()->route('customer.plugins.index') + ->with('error', 'Only rejected plugins can be resubmitted.'); + } + + $plugin->resubmit(); + + return redirect()->route('customer.plugins.index') + ->with('success', 'Your plugin has been resubmitted for review!'); + } +} diff --git a/app/Http/Controllers/PluginDirectoryController.php b/app/Http/Controllers/PluginDirectoryController.php new file mode 100644 index 00000000..f5db3ead --- /dev/null +++ b/app/Http/Controllers/PluginDirectoryController.php @@ -0,0 +1,31 @@ +approved() + ->featured() + ->latest() + ->take(3) + ->get(); + + $latestPlugins = Plugin::query() + ->approved() + ->where('featured', false) + ->latest() + ->take(3) + ->get(); + + return view('plugins', [ + 'featuredPlugins' => $featuredPlugins, + 'latestPlugins' => $latestPlugins, + ]); + } +} diff --git a/app/Http/Requests/SubmitPluginRequest.php b/app/Http/Requests/SubmitPluginRequest.php new file mode 100644 index 00000000..b6da8de3 --- /dev/null +++ b/app/Http/Requests/SubmitPluginRequest.php @@ -0,0 +1,47 @@ + [ + 'required', + 'string', + 'max:255', + 'regex:/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*$/i', + 'unique:plugins,name', + ], + 'type' => ['required', 'string', Rule::enum(PluginType::class)], + 'anystack_id' => [ + 'nullable', + 'required_if:type,paid', + 'string', + 'max:255', + ], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Please enter your plugin\'s Composer package name.', + 'name.regex' => 'Please enter a valid Composer package name (e.g., vendor/package-name).', + 'name.unique' => 'This plugin has already been submitted.', + 'type.required' => 'Please select whether your plugin is free or paid.', + 'type.enum' => 'Please select a valid plugin type.', + 'anystack_id.required_if' => 'Please enter your Anystack Product ID for paid plugins.', + ]; + } +} diff --git a/app/Http/Requests/UpdatePluginDescriptionRequest.php b/app/Http/Requests/UpdatePluginDescriptionRequest.php new file mode 100644 index 00000000..ccefdc9e --- /dev/null +++ b/app/Http/Requests/UpdatePluginDescriptionRequest.php @@ -0,0 +1,34 @@ +> + */ + public function rules(): array + { + return [ + 'description' => ['required', 'string', 'max:1000'], + ]; + } + + /** + * @return array + */ + public function messages(): array + { + return [ + 'description.required' => 'Please provide a description for your plugin.', + 'description.max' => 'The description must not exceed 1000 characters.', + ]; + } +} diff --git a/app/Livewire/PluginDirectory.php b/app/Livewire/PluginDirectory.php new file mode 100644 index 00000000..273b1ee2 --- /dev/null +++ b/app/Livewire/PluginDirectory.php @@ -0,0 +1,51 @@ +resetPage(); + } + + public function clearSearch(): void + { + $this->search = ''; + $this->resetPage(); + } + + public function render(): View + { + $plugins = Plugin::query() + ->approved() + ->when($this->search, function ($query) { + $query->where(function ($q) { + $q->where('name', 'like', "%{$this->search}%") + ->orWhere('description', 'like', "%{$this->search}%"); + }); + }) + ->orderByDesc('featured') + ->latest() + ->paginate(15); + + return view('livewire.plugin-directory', [ + 'plugins' => $plugins, + ]); + } +} diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php new file mode 100644 index 00000000..13c48816 --- /dev/null +++ b/app/Models/Plugin.php @@ -0,0 +1,229 @@ + PluginStatus::class, + 'type' => PluginType::class, + 'approved_at' => 'datetime', + 'featured' => 'boolean', + ]; + + protected static function booted(): void + { + static::created(function (Plugin $plugin) { + $plugin->recordActivity( + PluginActivityType::Submitted, + null, + PluginStatus::Pending, + null, + $plugin->user_id + ); + }); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * @return BelongsTo + */ + public function approvedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * @return HasMany + */ + public function activities(): HasMany + { + return $this->hasMany(PluginActivity::class)->orderBy('created_at', 'desc'); + } + + public function isPending(): bool + { + return $this->status === PluginStatus::Pending; + } + + public function isApproved(): bool + { + return $this->status === PluginStatus::Approved; + } + + public function isRejected(): bool + { + return $this->status === PluginStatus::Rejected; + } + + public function isFree(): bool + { + return $this->type === PluginType::Free; + } + + public function isPaid(): bool + { + return $this->type === PluginType::Paid; + } + + public function isFeatured(): bool + { + return $this->featured; + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeApproved(Builder $query): Builder + { + return $query->where('status', PluginStatus::Approved); + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeFeatured(Builder $query): Builder + { + return $query->where('featured', true); + } + + public function getPackagistUrl(): string + { + return "https://packagist.org/packages/{$this->name}"; + } + + public function getGithubUrl(): string + { + return "https://github.com/{$this->name}"; + } + + public function getAnystackUrl(): ?string + { + if (! $this->anystack_id) { + return null; + } + + return "https://anystack.sh/products/{$this->anystack_id}"; + } + + public function approve(int $approvedById): void + { + $previousStatus = $this->status; + + $this->update([ + 'status' => PluginStatus::Approved, + 'approved_at' => now(), + 'approved_by' => $approvedById, + 'rejection_reason' => null, + ]); + + $this->recordActivity( + PluginActivityType::Approved, + $previousStatus, + PluginStatus::Approved, + null, + $approvedById + ); + + $this->user->notify(new PluginApproved($this)); + } + + public function reject(string $reason, int $rejectedById): void + { + $previousStatus = $this->status; + + $this->update([ + 'status' => PluginStatus::Rejected, + 'rejection_reason' => $reason, + 'approved_at' => null, + 'approved_by' => $rejectedById, + ]); + + $this->recordActivity( + PluginActivityType::Rejected, + $previousStatus, + PluginStatus::Rejected, + $reason, + $rejectedById + ); + + $this->user->notify(new PluginRejected($this)); + } + + public function resubmit(): void + { + $previousStatus = $this->status; + + $this->update([ + 'status' => PluginStatus::Pending, + 'rejection_reason' => null, + 'approved_at' => null, + 'approved_by' => null, + ]); + + $this->recordActivity( + PluginActivityType::Resubmitted, + $previousStatus, + PluginStatus::Pending, + null, + $this->user_id + ); + } + + public function updateDescription(string $description, int $updatedById): void + { + $oldDescription = $this->description; + + $this->update([ + 'description' => $description, + ]); + + $this->activities()->create([ + 'type' => PluginActivityType::DescriptionUpdated, + 'from_status' => $this->status->value, + 'to_status' => $this->status->value, + 'note' => $oldDescription ? "Changed from: {$oldDescription}" : 'Initial description set', + 'causer_id' => $updatedById, + ]); + } + + protected function recordActivity( + PluginActivityType $type, + ?PluginStatus $fromStatus, + PluginStatus $toStatus, + ?string $note, + ?int $causerId + ): void { + $this->activities()->create([ + 'type' => $type, + 'from_status' => $fromStatus?->value, + 'to_status' => $toStatus->value, + 'note' => $note, + 'causer_id' => $causerId, + ]); + } +} diff --git a/app/Models/PluginActivity.php b/app/Models/PluginActivity.php new file mode 100644 index 00000000..b41094ea --- /dev/null +++ b/app/Models/PluginActivity.php @@ -0,0 +1,35 @@ + PluginActivityType::class, + 'from_status' => PluginStatus::class, + 'to_status' => PluginStatus::class, + ]; + + /** + * @return BelongsTo + */ + public function plugin(): BelongsTo + { + return $this->belongsTo(Plugin::class); + } + + /** + * @return BelongsTo + */ + public function causer(): BelongsTo + { + return $this->belongsTo(User::class, 'causer_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index e1fec6ab..54f94e41 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -55,6 +55,14 @@ public function wallOfLoveSubmissions(): HasMany return $this->hasMany(WallOfLoveSubmission::class); } + /** + * @return HasMany + */ + public function plugins(): HasMany + { + return $this->hasMany(Plugin::class); + } + public function getFirstNameAttribute(): ?string { if (empty($this->name)) { diff --git a/app/Notifications/PluginApproved.php b/app/Notifications/PluginApproved.php new file mode 100644 index 00000000..65eec4d8 --- /dev/null +++ b/app/Notifications/PluginApproved.php @@ -0,0 +1,54 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject('Your Plugin Has Been Approved!') + ->greeting('Great news!') + ->line("Your plugin **{$this->plugin->name}** has been approved and is now listed in the NativePHP Plugin Directory.") + ->action('View Plugin Directory', url('/plugins')) + ->line('Thank you for contributing to the NativePHP ecosystem!'); + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'plugin_id' => $this->plugin->id, + 'plugin_name' => $this->plugin->name, + ]; + } +} diff --git a/app/Notifications/PluginRejected.php b/app/Notifications/PluginRejected.php new file mode 100644 index 00000000..a1951f51 --- /dev/null +++ b/app/Notifications/PluginRejected.php @@ -0,0 +1,57 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject('Plugin Submission Update') + ->greeting('Hello,') + ->line("Unfortunately, your plugin **{$this->plugin->name}** was not approved for the NativePHP Plugin Directory.") + ->line('**Reason:**') + ->line($this->plugin->rejection_reason) + ->action('View Your Plugins', url('/customer/plugins')) + ->line('If you have questions about this decision, please reach out to us.'); + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'plugin_id' => $this->plugin->id, + 'plugin_name' => $this->plugin->name, + 'rejection_reason' => $this->plugin->rejection_reason, + ]; + } +} diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php new file mode 100644 index 00000000..14f67b92 --- /dev/null +++ b/database/factories/PluginFactory.php @@ -0,0 +1,186 @@ + + */ +class PluginFactory extends Factory +{ + protected $model = Plugin::class; + + /** + * @var array + */ + protected array $pluginPrefixes = [ + 'nativephp', + 'laravel', + 'acme', + 'awesome', + 'super', + 'native', + 'mobile', + 'app', + ]; + + /** + * @var array + */ + protected array $pluginSuffixes = [ + 'camera', + 'biometrics', + 'push-notifications', + 'geolocation', + 'bluetooth', + 'nfc', + 'contacts', + 'calendar', + 'health-kit', + 'share', + 'in-app-purchase', + 'admob', + 'analytics', + 'crashlytics', + 'deep-links', + 'local-auth', + 'secure-storage', + 'file-picker', + 'image-picker', + 'video-player', + 'audio-player', + 'speech-to-text', + 'text-to-speech', + 'barcode-scanner', + 'qr-code', + 'maps', + 'payments', + 'social-auth', + 'firebase', + 'sentry', + 'offline-sync', + 'background-tasks', + 'sensors', + 'haptics', + 'clipboard', + 'device-info', + 'network-info', + 'battery', + 'screen-brightness', + 'orientation', + 'keyboard', + 'status-bar', + 'splash-screen', + 'app-icon', + 'widgets', + ]; + + /** + * @var array + */ + protected array $descriptions = [ + 'A powerful plugin that integrates seamlessly with your NativePHP Mobile application, providing essential native functionality.', + 'Easily add native capabilities to your Laravel mobile app with this simple-to-use plugin.', + 'This plugin bridges the gap between PHP and native platform APIs, giving you full control.', + 'Unlock advanced mobile features with minimal configuration. Works on both iOS and Android.', + 'A production-ready plugin built with performance and reliability in mind.', + 'Simplify complex native integrations with this well-documented and tested plugin.', + 'Built by experienced mobile developers, this plugin follows best practices for both platforms.', + 'Zero-config setup that just works. Install via Composer and start using immediately.', + 'Comprehensive feature set with granular permissions control for enhanced security.', + 'Lightweight and fast, this plugin has minimal impact on your app\'s performance.', + ]; + + public function definition(): array + { + $vendor = fake()->randomElement($this->pluginPrefixes); + $package = fake()->randomElement($this->pluginSuffixes); + + return [ + 'user_id' => User::factory(), + 'name' => fake()->unique()->numerify("{$vendor}/{$package}-###"), + 'description' => fake()->randomElement($this->descriptions), + 'type' => PluginType::Free, + 'status' => PluginStatus::Pending, + 'featured' => false, + 'anystack_id' => null, + 'rejection_reason' => null, + 'approved_at' => null, + 'approved_by' => null, + 'created_at' => fake()->dateTimeBetween('-6 months', 'now'), + 'updated_at' => fn (array $attrs) => $attrs['created_at'], + ]; + } + + public function pending(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => PluginStatus::Pending, + 'approved_at' => null, + 'approved_by' => null, + 'rejection_reason' => null, + ]); + } + + public function approved(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => PluginStatus::Approved, + 'approved_at' => fake()->dateTimeBetween($attributes['created_at'], 'now'), + 'approved_by' => User::factory(), + 'rejection_reason' => null, + ]); + } + + public function rejected(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => PluginStatus::Rejected, + 'approved_at' => null, + 'approved_by' => null, + 'rejection_reason' => fake()->randomElement([ + 'Package not found on Packagist. Please ensure your package is published.', + 'Plugin does not meet our quality standards. Please review our plugin guidelines.', + 'Missing required documentation. Please add a README with installation instructions.', + 'Security concerns identified. Please address the issues and resubmit.', + 'Plugin name conflicts with an existing package. Please choose a different name.', + 'Incomplete implementation. Some advertised features are not working as expected.', + ]), + ]); + } + + public function featured(): static + { + return $this->state(fn (array $attributes) => [ + 'featured' => true, + ]); + } + + public function free(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => PluginType::Free, + 'anystack_id' => null, + ]); + } + + public function paid(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => PluginType::Paid, + 'anystack_id' => fake()->uuid(), + ]); + } + + public function withoutDescription(): static + { + return $this->state(fn (array $attributes) => [ + 'description' => null, + ]); + } +} diff --git a/database/migrations/2025_12_03_124822_create_plugins_table.php b/database/migrations/2025_12_03_124822_create_plugins_table.php new file mode 100644 index 00000000..c5549804 --- /dev/null +++ b/database/migrations/2025_12_03_124822_create_plugins_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name'); // Composer package name e.g. vendor/package-name + $table->string('type'); // free or paid + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('status')->default('pending'); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->unique('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('plugins'); + } +}; diff --git a/database/migrations/2025_12_03_141900_add_anystack_id_and_rejection_reason_to_plugins_table.php b/database/migrations/2025_12_03_141900_add_anystack_id_and_rejection_reason_to_plugins_table.php new file mode 100644 index 00000000..000cef6f --- /dev/null +++ b/database/migrations/2025_12_03_141900_add_anystack_id_and_rejection_reason_to_plugins_table.php @@ -0,0 +1,29 @@ +string('anystack_id')->nullable()->after('type'); + $table->text('rejection_reason')->nullable()->after('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn(['anystack_id', 'rejection_reason']); + }); + } +}; diff --git a/database/migrations/2025_12_03_154716_create_plugin_activities_table.php b/database/migrations/2025_12_03_154716_create_plugin_activities_table.php new file mode 100644 index 00000000..2ec3fe42 --- /dev/null +++ b/database/migrations/2025_12_03_154716_create_plugin_activities_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('plugin_id')->constrained()->cascadeOnDelete(); + $table->string('type'); // submitted, resubmitted, approved, rejected + $table->string('from_status')->nullable(); + $table->string('to_status'); + $table->text('note')->nullable(); // rejection reason or other notes + $table->foreignId('causer_id')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['plugin_id', 'created_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('plugin_activities'); + } +}; diff --git a/database/migrations/2025_12_03_161416_add_featured_to_plugins_table.php b/database/migrations/2025_12_03_161416_add_featured_to_plugins_table.php new file mode 100644 index 00000000..f5d5f55d --- /dev/null +++ b/database/migrations/2025_12_03_161416_add_featured_to_plugins_table.php @@ -0,0 +1,30 @@ +boolean('featured')->default(false)->after('status'); + $table->index(['status', 'featured']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropIndex(['status', 'featured']); + $table->dropColumn('featured'); + }); + } +}; diff --git a/database/migrations/2025_12_03_175340_add_description_to_plugins_table.php b/database/migrations/2025_12_03_175340_add_description_to_plugins_table.php new file mode 100644 index 00000000..368dc1f7 --- /dev/null +++ b/database/migrations/2025_12_03_175340_add_description_to_plugins_table.php @@ -0,0 +1,28 @@ +text('description')->nullable()->after('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('description'); + }); + } +}; diff --git a/database/seeders/PluginSeeder.php b/database/seeders/PluginSeeder.php new file mode 100644 index 00000000..4e86658d --- /dev/null +++ b/database/seeders/PluginSeeder.php @@ -0,0 +1,127 @@ +take(20)->get(); + + if ($users->count() < 20) { + $additionalUsers = User::factory() + ->count(20 - $users->count()) + ->create(); + + $users = $users->merge($additionalUsers); + } + + // Get or create an admin user for approvals + $admin = User::query() + ->where('email', 'admin@example.com') + ->first() ?? User::factory()->create([ + 'name' => 'Admin User', + 'email' => 'admin@example.com', + ]); + + // Create 10 featured approved plugins (free) + Plugin::factory() + ->count(10) + ->approved() + ->featured() + ->free() + ->create([ + 'user_id' => fn () => $users->random()->id, + 'approved_by' => $admin->id, + ]); + + // Create 5 featured approved plugins (paid) + Plugin::factory() + ->count(5) + ->approved() + ->featured() + ->paid() + ->create([ + 'user_id' => fn () => $users->random()->id, + 'approved_by' => $admin->id, + ]); + + // Create 30 approved free plugins (not featured) + Plugin::factory() + ->count(30) + ->approved() + ->free() + ->create([ + 'user_id' => fn () => $users->random()->id, + 'approved_by' => $admin->id, + ]); + + // Create 15 approved paid plugins (not featured) + Plugin::factory() + ->count(15) + ->approved() + ->paid() + ->create([ + 'user_id' => fn () => $users->random()->id, + 'approved_by' => $admin->id, + ]); + + // Create 20 pending plugins (mix of free and paid) + Plugin::factory() + ->count(15) + ->pending() + ->free() + ->create([ + 'user_id' => fn () => $users->random()->id, + ]); + + Plugin::factory() + ->count(5) + ->pending() + ->paid() + ->create([ + 'user_id' => fn () => $users->random()->id, + ]); + + // Create 15 rejected plugins + Plugin::factory() + ->count(10) + ->rejected() + ->free() + ->create([ + 'user_id' => fn () => $users->random()->id, + ]); + + Plugin::factory() + ->count(5) + ->rejected() + ->paid() + ->create([ + 'user_id' => fn () => $users->random()->id, + ]); + + // Create a few approved plugins without descriptions + Plugin::factory() + ->count(5) + ->approved() + ->free() + ->withoutDescription() + ->create([ + 'user_id' => fn () => $users->random()->id, + 'approved_by' => $admin->id, + ]); + + $this->command->info('Created plugins:'); + $this->command->info(' - 15 featured approved (10 free, 5 paid)'); + $this->command->info(' - 45 approved non-featured (30 free, 15 paid)'); + $this->command->info(' - 5 approved without descriptions'); + $this->command->info(' - 20 pending (15 free, 5 paid)'); + $this->command->info(' - 15 rejected (10 free, 5 paid)'); + $this->command->info(' Total: 100 plugins (65 approved)'); + } +} diff --git a/resources/views/components/icons/puzzle.blade.php b/resources/views/components/icons/puzzle.blade.php new file mode 100644 index 00000000..2ece32d5 --- /dev/null +++ b/resources/views/components/icons/puzzle.blade.php @@ -0,0 +1,12 @@ + + + diff --git a/resources/views/components/navbar/device-dropdowns.blade.php b/resources/views/components/navbar/device-dropdowns.blade.php index 320d676b..06c5e162 100644 --- a/resources/views/components/navbar/device-dropdowns.blade.php +++ b/resources/views/components/navbar/device-dropdowns.blade.php @@ -18,6 +18,13 @@ icon="dollar-circle" icon-class="size-5.5" /> + {{-- 👇 Hidden temporarily --}} {{-- +
+
+ +
+ @if ($plugin->isPaid()) + + Paid + + @else + + Free + + @endif +
+ +
+

+ {{ $plugin->name }} +

+ @if ($plugin->description) +

+ {{ $plugin->description }} +

+ @endif +
+ +
+ @if ($plugin->isFree()) + + View on Packagist + + + + + @else + + View on Anystack + + + + + @endif +
+ diff --git a/resources/views/customer/licenses/index.blade.php b/resources/views/customer/licenses/index.blade.php index c7fc229d..e1493db4 100644 --- a/resources/views/customer/licenses/index.blade.php +++ b/resources/views/customer/licenses/index.blade.php @@ -11,6 +11,10 @@

+ + + Plugins + Manage Your Subscription diff --git a/resources/views/customer/plugins/create.blade.php b/resources/views/customer/plugins/create.blade.php new file mode 100644 index 00000000..372efcc7 --- /dev/null +++ b/resources/views/customer/plugins/create.blade.php @@ -0,0 +1,220 @@ + +
+ {{-- Header --}} +
+
+
+
+ +

Submit Your Plugin

+

+ Add your plugin to the NativePHP Plugin Directory +

+
+
+
+
+ + {{-- Content --}} +
+
+ @csrf + + {{-- Plugin Name --}} +
+

Plugin Details

+

+ Enter your plugin's Composer package name. +

+ +
+ +
+ +
+ @error('name') +

{{ $message }}

+ @else +

+ This should match your package name in composer.json +

+ @enderror +
+
+ + {{-- Plugin Type --}} +
+

Plugin Type

+

+ Is your plugin free or paid? +

+ +
+ {{-- Free Option --}} + + + {{-- Paid Option --}} + +
+ + @error('type') +

{{ $message }}

+ @enderror +
+ + {{-- Anystack Instructions (shown for paid plugins) --}} +
+
+
+ + + +
+
+

+ Anystack Setup Required +

+
+

+ To sell your plugin through the NativePHP Plugin Directory, you'll need to set up your product on Anystack and join our affiliate program. +

+
    +
  1. + Create an Anystack account at + anystack.sh +
  2. +
  3. + Set up your product with your plugin's details, pricing, and license configuration +
  4. +
  5. + Join our affiliate program by going to the Advertising section in your Anystack dashboard and applying to the "NativePHP Plugin Directory" program +
  6. +
+
+

+ Commission: NativePHP takes a 30% commission on sales through the Plugin Directory. Anystack also charges their standard transaction fees. +

+
+
+
+
+ + {{-- Anystack Product ID Input --}} +
+ +
+ +
+ @error('anystack_id') +

{{ $message }}

+ @else +

+ You can find this in the URL when viewing your product in Anystack (e.g., anystack.sh/products/a1b2c3d4-e5f6-...) +

+ @enderror +
+
+ + {{-- Submit Button --}} +
+ + Cancel + + +
+
+
+
+
diff --git a/resources/views/customer/plugins/index.blade.php b/resources/views/customer/plugins/index.blade.php new file mode 100644 index 00000000..0005b7fa --- /dev/null +++ b/resources/views/customer/plugins/index.blade.php @@ -0,0 +1,189 @@ + +
+ {{-- Header --}} +
+
+
+
+

Plugins

+

+ Extend NativePHP Mobile with powerful native features +

+
+ +
+
+
+ + {{-- Content --}} +
+ {{-- Action Cards --}} +
+ {{-- Submit Plugin Card (Most Prominent) --}} +
+
+
+
+ + + +
+

Submit Your Plugin

+

+ Built a plugin? Submit it to the NativePHP Plugin Directory and share it with the community. +

+ + Submit a Plugin + + + + +
+
+ + {{-- Browse Plugins Card --}} +
+
+ +
+

Browse Plugins

+

+ Discover plugins built by the community to add native features to your mobile apps. +

+ + View Directory + + + + +
+ + {{-- Learn to Build Card --}} +
+
+ + + +
+

Learn to Build Plugins

+

+ Read the documentation to learn how to create your own NativePHP Mobile plugins. +

+ + Read the Docs + + + + +
+
+ + {{-- Success Message --}} + @if (session('success')) +
+
+
+ + + +
+
+

{{ session('success') }}

+
+
+
+ @endif + + {{-- Submitted Plugins List --}} + @if ($plugins->count() > 0) +
+

Your Submitted Plugins

+

Track the status of your plugin submissions.

+ +
+
    + @foreach ($plugins as $plugin) +
  • +
    +
    +
    + @if ($plugin->isPending()) +
    + @elseif ($plugin->isApproved()) +
    + @else +
    + @endif +
    +
    +

    + {{ $plugin->name }} +

    +

    + {{ $plugin->type->label() }} plugin • Submitted {{ $plugin->created_at->diffForHumans() }} +

    +
    +
    +
    + @if ($plugin->isPending()) + + Pending Review + + @elseif ($plugin->isApproved()) + + Approved + + @else + + Rejected + + @endif + + Edit + + + + +
    +
    + + {{-- Rejection Reason --}} + @if ($plugin->isRejected() && $plugin->rejection_reason) +
    +
    +
    + + + +
    +
    +

    Rejection Reason

    +

    {{ $plugin->rejection_reason }}

    +
    +
    + @csrf + +
    +
    +
    +
    +
    + @endif +
  • + @endforeach +
+
+
+ @endif +
+
+
diff --git a/resources/views/customer/plugins/show.blade.php b/resources/views/customer/plugins/show.blade.php new file mode 100644 index 00000000..63041593 --- /dev/null +++ b/resources/views/customer/plugins/show.blade.php @@ -0,0 +1,135 @@ + +
+ {{-- Header --}} +
+
+
+
+

Edit Plugin

+

+ {{ $plugin->name }} +

+
+ + Back to Plugins + +
+
+
+ + {{-- Content --}} +
+ {{-- Success Message --}} + @if (session('success')) +
+
+
+ + + +
+
+

{{ session('success') }}

+
+
+
+ @endif + + {{-- Plugin Status --}} +
+
+
+
+ +
+
+

{{ $plugin->name }}

+

{{ $plugin->type->label() }} plugin

+
+
+ @if ($plugin->isPending()) + + Pending Review + + @elseif ($plugin->isApproved()) + + Approved + + @else + + Rejected + + @endif +
+
+ + {{-- Description Form --}} +
+

Plugin Description

+

+ Describe what your plugin does. This will be displayed in the plugin directory. +

+ +
+ @csrf + @method('PATCH') + +
+ + + @error('description') +

{{ $message }}

+ @enderror +

+ Maximum 1000 characters +

+
+ +
+ +
+
+
+ + {{-- Rejection Reason --}} + @if ($plugin->isRejected() && $plugin->rejection_reason) +
+
+
+ + + +
+
+

Rejection Reason

+

{{ $plugin->rejection_reason }}

+
+
+ @csrf + +
+
+
+
+
+ @endif +
+
+
diff --git a/resources/views/livewire/plugin-directory.blade.php b/resources/views/livewire/plugin-directory.blade.php new file mode 100644 index 00000000..a8b6d5db --- /dev/null +++ b/resources/views/livewire/plugin-directory.blade.php @@ -0,0 +1,117 @@ +
+ {{-- Header --}} +
+
+

Plugin Directory

+

+ Browse all available plugins for NativePHP Mobile. +

+
+ + {{-- Search --}} +
+
+
+
+ + + +
+ + @if ($search) + + @endif +
+
+
+ + {{-- Results count --}} + @if ($search) +

+ {{ $plugins->total() }} {{ Str::plural('result', $plugins->total()) }} for "{{ $search }}" +

+ @endif +
+ + {{-- Plugin Grid --}} +
+ @if ($plugins->count() > 0) +
+ @foreach ($plugins as $plugin) + + @endforeach +
+ + {{-- Pagination --}} + @if ($plugins->hasPages()) +
+ {{ $plugins->links() }} +
+ @endif + @else +
+ +

No plugins found

+ @if ($search) +

+ No plugins match your search. Try a different term. +

+ + @else +

+ Be the first to submit a plugin to the directory! +

+ + Submit a Plugin + + @endif +
+ @endif +
+ + {{-- Back to plugins landing --}} +
+ +
+
diff --git a/resources/views/plugins.blade.php b/resources/views/plugins.blade.php new file mode 100644 index 00000000..3a7d3beb --- /dev/null +++ b/resources/views/plugins.blade.php @@ -0,0 +1,658 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Icon --}} +
+
+ +
+
+ + {{-- Title --}} +

+ + { + + Plugins + + } + +

+ + {{-- Subtitle --}} +

+ Extend your NativePHP Mobile apps with powerful native features. + Install with Composer. Build anything for iOS and Android. +

+ + {{-- Call to Action Buttons --}} +
+ {{-- Primary CTA - Browse Plugins --}} + + + {{-- Secondary CTA - Documentation --}} + +
+
+
+ + {{-- Featured Plugins Section --}} +
+
+

+ Featured Plugins +

+

+ Hand-picked plugins to supercharge your mobile apps. +

+ + {{-- Plugin Cards Grid --}} +
+ @forelse ($featuredPlugins as $plugin) + + @empty +
+ +

+ Featured plugins coming soon +

+
+ + + @endforelse +
+
+
+ + {{-- Latest Plugins Section --}} +
+
+

+ Latest Plugins +

+

+ Freshly released plugins from our community. +

+ + {{-- Plugin Cards Grid --}} +
+ @forelse ($latestPlugins as $plugin) + + @empty +
+ +

+ New plugins coming soon +

+
+ + + @endforelse +
+
+
+ + {{-- Benefits Section --}} +
+
+

+ Why Use Plugins? +

+

+ Unlock native capabilities without leaving Laravel. +

+
+ +
+ {{-- Card - Composer Install --}} + + + + + + + + One Command Install + + + Add native features with a single composer require. No Xcode or Android Studio knowledge required. + + + + {{-- Card - Build Anything --}} + + + + + + + + Build Anything + + + There's no limit to what plugins can do. Access any native API, sensor, or hardware feature on iOS and Android. + + + + {{-- Card - Auto-Registered --}} + + + + + + + + Auto-Registered + + + Plugins are automatically discovered and registered. Just enable them in your config and you're ready to go. + + + + {{-- Card - Platform Dependencies --}} + + + + + + + + Native Dependencies + + + Plugins can add Gradle dependencies, CocoaPods, and Swift Package Manager packages automatically. + + + + {{-- Card - Lifecycle Hooks --}} + + + + + + + + Build Lifecycle Hooks + + + Hook into critical moments in the build pipeline. Run custom logic before, during, or after builds. + + + + {{-- Card - Security --}} + + + + + + + + Security First + + + Security is our top priority. Plugins are sandboxed and permissions are explicit, keeping your users safe. + + +
+
+ + {{-- For Plugin Authors Section --}} +
+
+

+ Build & Sell Your Own Plugins +

+ +

+ Are you a Swift or Kotlin developer? Create plugins for the NativePHP community and generate revenue from your expertise. +

+ +
+
+
+ + + +
+
+

+ Write Swift & Kotlin +

+

+ Build the native code and PHP bridging layer. We handle the rest, mapping everything so it just works. +

+
+
+ +
+
+ + + +
+
+

+ Full Laravel Power +

+

+ Set permissions, create config files, publish views, and do everything a Laravel package can do. +

+
+
+ +
+
+ + + +
+
+

+ Generate Revenue +

+

+ Sell your plugins through our marketplace and earn money from your native development skills. +

+
+
+
+ + +
+
+ + {{-- Call to Action Section --}} +
+
+

+ Ready to Extend Your App? +

+ +

+ Discover plugins that add powerful native features to your NativePHP Mobile apps, or start building your own today. +

+ +
+ {{-- Primary CTA --}} + + + {{-- Secondary CTA --}} + +
+
+
+
+
diff --git a/resources/views/pricing.blade.php b/resources/views/pricing.blade.php index ea99d31d..602b703f 100644 --- a/resources/views/pricing.blade.php +++ b/resources/views/pricing.blade.php @@ -739,6 +739,17 @@ class="mx-auto flex w-full max-w-2xl flex-col items-center gap-4 pt-10"

+ +

+ You will get direct access to our GitHub repository for NativePHP for Mobile. This will allow you + to raise issues directly with the team, which are prioritized higher than issues we see on Discord. +

+

+ It also means you can try out features that are still in development before they're generally + available and help us to shape and refine them for release. +

+
+ diff --git a/routes/web.php b/routes/web.php index f538e489..cffee69e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,7 +4,9 @@ use App\Http\Controllers\ApplinksController; use App\Http\Controllers\Auth\CustomerAuthController; use App\Http\Controllers\CustomerLicenseController; +use App\Http\Controllers\CustomerPluginController; use App\Http\Controllers\CustomerSubLicenseController; +use App\Http\Controllers\PluginDirectoryController; use App\Http\Controllers\ShowBlogController; use App\Http\Controllers\ShowDocumentationController; use Illuminate\Support\Facades\Route; @@ -42,6 +44,8 @@ Route::view('privacy-policy', 'privacy-policy')->name('privacy-policy'); Route::view('terms-of-service', 'terms-of-service')->name('terms-of-service'); Route::view('partners', 'partners')->name('partners'); +Route::get('plugins', [PluginDirectoryController::class, 'index'])->name('plugins'); +Route::get('plugins/directory', App\Livewire\PluginDirectory::class)->name('plugins.directory'); Route::view('sponsor', 'sponsoring')->name('sponsoring'); Route::get('blog', [ShowBlogController::class, 'index'])->name('blog'); @@ -115,7 +119,7 @@ Route::get('callback', function (Illuminate\Http\Request $request) { $url = $request->query('url'); - if ($url && !str_starts_with($url, 'http')) { + if ($url && ! str_starts_with($url, 'http')) { return redirect()->away($url.'?token='.uuid_create()); } @@ -131,6 +135,14 @@ // Wall of Love submission Route::get('wall-of-love/create', [App\Http\Controllers\WallOfLoveSubmissionController::class, 'create'])->name('wall-of-love.create'); + // Plugin management + Route::get('plugins', [CustomerPluginController::class, 'index'])->name('plugins.index'); + Route::get('plugins/submit', [CustomerPluginController::class, 'create'])->name('plugins.create'); + Route::post('plugins', [CustomerPluginController::class, 'store'])->name('plugins.store'); + Route::get('plugins/{plugin}', [CustomerPluginController::class, 'show'])->name('plugins.show'); + Route::patch('plugins/{plugin}', [CustomerPluginController::class, 'update'])->name('plugins.update'); + Route::post('plugins/{plugin}/resubmit', [CustomerPluginController::class, 'resubmit'])->name('plugins.resubmit'); + // Billing portal Route::get('billing-portal', function (Illuminate\Http\Request $request) { $user = $request->user(); @@ -151,5 +163,4 @@ Route::post('licenses/{licenseKey}/sub-licenses/{subLicense}/send-email', [CustomerSubLicenseController::class, 'sendEmail'])->name('licenses.sub-licenses.send-email'); }); - Route::get('.well-known/assetlinks.json', [ApplinksController::class, 'assetLinks']); From 0b20c8f482cb141bbfbd3bc1a3d90c3fc55bd192 Mon Sep 17 00:00:00 2001 From: MasoodRehman Date: Fri, 5 Dec 2025 20:00:25 +0500 Subject: [PATCH 02/34] Update seeding command in databases.md (#233) The version ^2.0 of nativephp/desktop is using `php artisan native:seed` --- resources/views/docs/desktop/2/digging-deeper/databases.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/docs/desktop/2/digging-deeper/databases.md b/resources/views/docs/desktop/2/digging-deeper/databases.md index 41ad3064..cb64e252 100644 --- a/resources/views/docs/desktop/2/digging-deeper/databases.md +++ b/resources/views/docs/desktop/2/digging-deeper/databases.md @@ -83,10 +83,10 @@ php artisan native:migrate:fresh ## Seeding When developing, it's especially useful to seed your database with sample data. If you've set up -[Database Seeders](https://laravel.com/docs/seeding), you can run these using the `native:db:seed` command: +[Database Seeders](https://laravel.com/docs/seeding), you can run these using the `native:seed` command: ```shell -php artisan native:db:seed +php artisan native:seed ``` ## When not to use a database From 5507089676f3d6fa17b5233c4b7fc33c5142f38e Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Fri, 5 Dec 2025 14:54:32 -0500 Subject: [PATCH 03/34] Updates vite config --- resources/views/docs/mobile/2/getting-started/development.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/docs/mobile/2/getting-started/development.md b/resources/views/docs/mobile/2/getting-started/development.md index e232e08a..4d6aab95 100644 --- a/resources/views/docs/mobile/2/getting-started/development.md +++ b/resources/views/docs/mobile/2/getting-started/development.md @@ -42,13 +42,14 @@ To make your frontend build process works well with NativePHP, simply add the `n `vite.config.js`: ```js -import { nativephpMobile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] +import { nativephpMobile, nativephpHotFile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, + hotFile: nativephpHotFile(), }), tailwindcss(), nativephpMobile(), // [tl! focus] From f8563c31bc7a40bb2cc1f80b124461963fef5ae9 Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Fri, 5 Dec 2025 14:56:39 -0500 Subject: [PATCH 04/34] Updates // [tl! focus] --- resources/views/docs/mobile/2/getting-started/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/docs/mobile/2/getting-started/development.md b/resources/views/docs/mobile/2/getting-started/development.md index 4d6aab95..37542322 100644 --- a/resources/views/docs/mobile/2/getting-started/development.md +++ b/resources/views/docs/mobile/2/getting-started/development.md @@ -49,7 +49,7 @@ export default defineConfig({ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, - hotFile: nativephpHotFile(), + hotFile: nativephpHotFile(), // [tl! focus] }), tailwindcss(), nativephpMobile(), // [tl! focus] From 2ca9841c4f59cdb1092aeb25c7ab6b07db1c4f28 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 5 Dec 2025 21:16:55 +0000 Subject: [PATCH 05/34] Mobile release 2025-12-05 (#243) --- .../docs/mobile/2/concepts/authentication.md | 32 +++-- .../mobile/2/getting-started/development.md | 118 ++++++++++++------ 2 files changed, 101 insertions(+), 49 deletions(-) diff --git a/resources/views/docs/mobile/2/concepts/authentication.md b/resources/views/docs/mobile/2/concepts/authentication.md index c86ebd80..e0c54a7c 100644 --- a/resources/views/docs/mobile/2/concepts/authentication.md +++ b/resources/views/docs/mobile/2/concepts/authentication.md @@ -74,18 +74,34 @@ You will likely want to use an OAuth client library in your app to make interact When initiating the auth flow for the user, you should use the `Native\Mobile\Facades\Browser::auth()` API, as this is purpose-built for securely passing authorization codes back from the OAuth service to your app. -You should set your redirect URL to `nativephp://127.0.0.1/some/route`, where `some/route` is a route you've defined in -your app's routes that will be able to handle the auth code. +For this to work, you must set a `NATIVEPHP_DEEPLINK_SCHEME` that will be unique for your application on users' devices. -Note that the scheme of the redirect URL in this case is **always** `nativephp://`. This has nothing to do with any -custom deep link scheme you may have set for your app. It is only tied to the `Browser::auth()` session. +```dotenv +NATIVEPHP_DEEPLINK_SCHEME=myapp +``` + +Then you must define your redirect URL. It should match your scheme and the route in your app that will handle the callback +data. + +```php +Browser::auth('https://workos.com/my-company/auth?redirect=myapp://auth/handle') +``` + +Most services will expect you to pre-define your redirect URLs as a security feature. You should be able to provide your +exact URL, as this will be the most secure method. + +How you handle the response in your app depends on how that particular API operates and the needs of your application. diff --git a/resources/views/docs/mobile/2/getting-started/development.md b/resources/views/docs/mobile/2/getting-started/development.md index 37542322..90e3a39d 100644 --- a/resources/views/docs/mobile/2/getting-started/development.md +++ b/resources/views/docs/mobile/2/getting-started/development.md @@ -99,10 +99,42 @@ If you're familiar with these tools, you can easily open the projects using the php artisan native:open ``` +### Configuration + +You can configure the folders that the `watch` command pays attention to in your `config/nativephp.php` file: + +```php +'hot_reload' => [ + 'watch_paths' => [ + 'app', + 'routes', + 'config', + 'database', + // Make sure "public" is listed in your config [tl! highlight:1] + 'public', + ], +] +``` + + + + ## Hot Reloading We've tried to make compiling your apps as fast as possible, but when coming from the 'make a change; hit refresh'-world -of PHP development that we all love, compiling apps can feel like a slow and time-consuming process. +of typical browser-based PHP development that we all love, compiling apps can feel like a slow and time-consuming +process. Hot reloading aims to make your app development experience feel just like home. @@ -112,69 +144,73 @@ You can start hot reloading by running the following command: php artisan native:watch ``` -You can also pass the `--watch` option to the `native:run` command. + This will start a long-lived process that watches your application's source files for changes, pushing them into the emulator after any updates and reloading the current screen. -Use this in tandem with Vite's own HMR for the platform you wish to test on: +If you're using Vite, we'll also use your Node CLI tool of choice (`npm`, `bun`, `pnpm`, or `yarn`) to run Vite's HMR +server. -```shell -npm run dev -- --mode=ios +### Enabling HMR -npm run dev -- --mode=android -``` +To make HMR work, you'll need to add the `hot` file helper to your `laravel` plugin's config in your `vite.config.js`: -This is useful during development for quickly testing changes without re-compiling your entire app. When you make -changes to any files in your Laravel app, the web view will be reloaded and your changes should show almost immediately. +```js +import { nativephpMobile, nativephpHotFile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] -Vite HMR is perfect for apps that use SPA frameworks like Vue or React to build the UI. It even works on real devices, -not just simulators! As long as the device is on the same network as the development machine. +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + hotFile: nativephpHotFile(), // [tl! focus] + }), + tailwindcss(), + nativephpMobile(), + ] +}); +``` +#### Two at a time, baby! -### Configuration +If you're developing on macOS, you can run both Android and iOS watchers at the same time in separate terminals: -You can configure the folders that the `watch` command pays attention to in your `config/nativephp.php` file: +```shell +# Terminal 1 +php artisan native:watch ios -```php -'hot_reload' => [ - 'watch_paths' => [ - 'app', - 'routes', - 'config', - 'database', - // Make sure "public" is listed in your config [tl! highlight:1] - 'public', - ], -] +# Terminal 2 +php artisan native:watch android ``` -### Order matters - -Depending on which order you run these commands, you may find that hot reloading doesn't work immediately. It's often -best to get the commands running, get your app open, and then make a request to a new screen to allow your app to pick -up the `hot` file's presence and connect to the HMR server. +This way you can see your changes reflected in real-time on both platforms **at the same time**. Wild. + -
diff --git a/resources/views/plugins.blade.php b/resources/views/plugins.blade.php index 3a7d3beb..791b0c89 100644 --- a/resources/views/plugins.blade.php +++ b/resources/views/plugins.blade.php @@ -49,13 +49,14 @@ " class="mt-6 text-4xl md:text-5xl lg:text-6xl" > + Mobile { Plugins } - + Rock {{-- Subtitle --}} @@ -80,7 +81,7 @@ class="mt-6 text-4xl md:text-5xl lg:text-6xl" class="mx-auto mt-6 max-w-3xl text-lg text-gray-600 dark:text-zinc-400" > Extend your NativePHP Mobile apps with powerful native features. - Install with Composer. Build anything for iOS and Android. + Install with Composer. Build anything for iOS and Android.

{{-- Call to Action Buttons --}} @@ -131,6 +132,7 @@ class="flex items-center justify-center gap-2.5 rounded-xl bg-gray-200 px-6 py-4 {{-- Featured Plugins Section --}} + @if ($featuredPlugins->isNotEmpty())
+ @endif + + {{-- Plugin Bundles Section --}} + @if ($bundles->isNotEmpty()) +
+
+

+ Plugin Bundles +

+

+ Save money with curated plugin collections. +

+ + {{-- Bundle Cards Grid --}} +
+ @foreach ($bundles as $bundle) + + @endforeach +
+
+
+ @endif {{-- Latest Plugins Section --}}
@@ -479,7 +521,7 @@ class="rounded-2xl bg-gray-100 p-12 dark:bg-[#1a1a2e]"

- Are you a Swift or Kotlin developer? Create plugins for the NativePHP community and generate revenue from your expertise. + Know Swift or Kotlin? Create plugins for the NativePHP community and generate revenue from your expertise.

@@ -556,7 +598,7 @@ class="size-5 shrink-0"

- Generate Revenue + Sell Your Plugins (Soon!)

Sell your plugins through our marketplace and earn money from your native development skills. diff --git a/resources/views/plugins/purchase-success.blade.php b/resources/views/plugins/purchase-success.blade.php new file mode 100644 index 00000000..a1e98dc1 --- /dev/null +++ b/resources/views/plugins/purchase-success.blade.php @@ -0,0 +1,150 @@ + +

+
+ {{-- Loading State --}} + + + {{-- Success State --}} + + + {{-- Timeout/Error State --}} + +
+
+ diff --git a/resources/views/plugins/purchase.blade.php b/resources/views/plugins/purchase.blade.php new file mode 100644 index 00000000..a5c80f31 --- /dev/null +++ b/resources/views/plugins/purchase.blade.php @@ -0,0 +1,174 @@ + +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Back button --}} + + + {{-- Plugin icon and title --}} +
+
+ +
+
+

+ Purchase {{ $plugin->name }} +

+ @if ($plugin->description) +

+ {{ $plugin->description }} +

+ @endif +
+
+
+ + + + {{-- Session Messages --}} + @if (session('error')) +
+

{{ session('error') }}

+
+ @endif + + {{-- Purchase Card --}} +
+

Order Summary

+ +
+ {{-- Plugin Info --}} +
+
+

{{ $plugin->name }}

+

Lifetime access

+
+
+ @if ($discountPercent > 0) +

+ ${{ number_format($originalAmount / 100, 2) }} +

+ @endif +

+ @if ($discountPercent === 100) + Free + @else + ${{ number_format($discountedAmount / 100, 2) }} + @endif +

+
+
+ + {{-- Discount Badge --}} + @if ($discountPercent > 0 && $discountPercent < 100) +
+
+ + + +

+ {{ $discountPercent }}% Early Adopter Discount Applied +

+
+
+ @elseif ($discountPercent === 100) +
+
+ + + +

+ Included with your Early Adopter Benefits +

+
+
+ @endif + + {{-- Divider --}} +
+
+

Total

+

+ @if ($discountPercent === 100) + Free + @else + ${{ number_format($discountedAmount / 100, 2) }} + @endif +

+
+
+
+ + {{-- Purchase Button --}} +
+ @if ($discountPercent === 100) +
+ @csrf + +
+ @else +
+ @csrf + +
+ @endif +
+ + {{-- Payment Info --}} +
+ + + + Secure payment via Stripe +
+
+ + {{-- What You Get --}} +
+

What's Included

+
    +
  • + + + + Lifetime access to plugin updates +
  • +
  • + + + + Install via Composer from plugins.nativephp.com +
  • +
  • + + + + Access to source code +
  • +
+
+
+
diff --git a/routes/api.php b/routes/api.php index 24843d09..fdf7d628 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ group(function () { + Route::prefix('plugins')->name('api.plugins.')->group(function () { + Route::get('/access', [PluginAccessController::class, 'index'])->name('access'); + Route::get('/access/{vendor}/{package}', [PluginAccessController::class, 'checkAccess'])->name('access.check'); + }); + Route::post('/licenses', [LicenseController::class, 'store']); Route::get('/licenses/{key}', [LicenseController::class, 'show']); Route::get('/licenses', [LicenseController::class, 'index']); diff --git a/routes/web.php b/routes/web.php index 2add5b90..309605a6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,13 +1,19 @@ name('terms-of-service'); Route::view('partners', 'partners')->name('partners'); Route::view('build-my-app', 'build-my-app')->name('build-my-app'); -Route::get('plugins', [PluginDirectoryController::class, 'index'])->name('plugins'); -Route::get('plugins/directory', App\Livewire\PluginDirectory::class)->name('plugins.directory'); -Route::get('plugins/{plugin}', [PluginDirectoryController::class, 'show'])->name('plugins.show'); + +// Public plugin directory routes +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function () { + Route::get('plugins', [PluginDirectoryController::class, 'index'])->name('plugins'); + Route::get('plugins/directory', App\Livewire\PluginDirectory::class)->name('plugins.directory'); + Route::get('plugins/{plugin}', [PluginDirectoryController::class, 'show'])->name('plugins.show'); +}); + Route::view('sponsor', 'sponsoring')->name('sponsoring'); Route::view('vs-react-native-expo', 'vs-react-native-expo')->name('vs-react-native-expo'); Route::view('vs-flutter', 'vs-flutter')->name('vs-flutter'); @@ -152,18 +163,23 @@ Route::get('reset-password/{token}', [CustomerAuthController::class, 'showResetPassword'])->name('password.reset'); Route::post('reset-password', [CustomerAuthController::class, 'resetPassword'])->name('password.update'); + + Route::get('auth/github/login', [App\Http\Controllers\GitHubAuthController::class, 'redirect'])->name('login.github'); }); Route::post('logout', [CustomerAuthController::class, 'logout']) ->middleware(EnsureFeaturesAreActive::using(ShowAuthButtons::class)) ->name('customer.logout'); -// GitHub OAuth routes +// GitHub OAuth callback (no auth required - handles both login and linking) +Route::get('auth/github/callback', [App\Http\Controllers\GitHubIntegrationController::class, 'handleCallback'])->name('github.callback'); + +// GitHub OAuth routes (auth required) Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->group(function () { Route::get('auth/github', [App\Http\Controllers\GitHubIntegrationController::class, 'redirectToGitHub'])->name('github.redirect'); - Route::get('auth/github/callback', [App\Http\Controllers\GitHubIntegrationController::class, 'handleCallback'])->name('github.callback'); Route::post('customer/github/request-access', [App\Http\Controllers\GitHubIntegrationController::class, 'requestRepoAccess'])->name('github.request-access'); Route::delete('customer/github/disconnect', [App\Http\Controllers\GitHubIntegrationController::class, 'disconnect'])->name('github.disconnect'); + Route::get('customer/github/repositories', [App\Http\Controllers\GitHubIntegrationController::class, 'repositories'])->name('github.repositories'); }); // Discord OAuth routes @@ -183,9 +199,15 @@ return response('Goodbye'); })->name('callback'); +// Dashboard route +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)]) + ->get('dashboard', [CustomerLicenseController::class, 'index']) + ->name('dashboard'); + // Customer license management routes Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->prefix('customer')->name('customer.')->group(function () { - Route::get('licenses', [CustomerLicenseController::class, 'index'])->name('licenses'); + // Redirect old licenses URL to dashboard + Route::redirect('licenses', '/dashboard')->name('licenses'); Route::view('integrations', 'customer.integrations')->name('integrations'); Route::get('licenses/{licenseKey}', [CustomerLicenseController::class, 'show'])->name('licenses.show'); Route::patch('licenses/{licenseKey}', [CustomerLicenseController::class, 'update'])->name('licenses.update'); @@ -199,12 +221,18 @@ Route::get('showcase/{showcase}/edit', [App\Http\Controllers\CustomerShowcaseController::class, 'edit'])->name('showcase.edit'); // Plugin management - Route::get('plugins', [CustomerPluginController::class, 'index'])->name('plugins.index'); - Route::get('plugins/submit', [CustomerPluginController::class, 'create'])->name('plugins.create'); - Route::post('plugins', [CustomerPluginController::class, 'store'])->name('plugins.store'); - Route::get('plugins/{plugin}', [CustomerPluginController::class, 'show'])->name('plugins.show'); - Route::patch('plugins/{plugin}', [CustomerPluginController::class, 'update'])->name('plugins.update'); - Route::post('plugins/{plugin}/resubmit', [CustomerPluginController::class, 'resubmit'])->name('plugins.resubmit'); + Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function () { + Route::get('plugins', [CustomerPluginController::class, 'index'])->name('plugins.index'); + Route::get('plugins/submit', [CustomerPluginController::class, 'create'])->name('plugins.create'); + Route::post('plugins', [CustomerPluginController::class, 'store'])->name('plugins.store'); + Route::get('plugins/{plugin}', [CustomerPluginController::class, 'show'])->name('plugins.show'); + Route::patch('plugins/{plugin}', [CustomerPluginController::class, 'update'])->name('plugins.update'); + Route::post('plugins/{plugin}/resubmit', [CustomerPluginController::class, 'resubmit'])->name('plugins.resubmit'); + Route::post('plugins/{plugin}/logo', [CustomerPluginController::class, 'updateLogo'])->name('plugins.logo.update'); + Route::delete('plugins/{plugin}/logo', [CustomerPluginController::class, 'deleteLogo'])->name('plugins.logo.delete'); + Route::patch('plugins/{plugin}/price', [CustomerPluginController::class, 'updatePrice'])->name('plugins.price.update'); + Route::patch('plugins/display-name', [CustomerPluginController::class, 'updateDisplayName'])->name('plugins.display-name'); + }); // Billing portal Route::get('billing-portal', function (Illuminate\Http\Request $request) { @@ -215,7 +243,7 @@ $user->createAsStripeCustomer(); } - return $user->redirectToBillingPortal(route('customer.licenses')); + return $user->redirectToBillingPortal(route('dashboard')); })->name('billing-portal'); // Sub-license management routes @@ -229,3 +257,53 @@ Route::get('.well-known/assetlinks.json', [ApplinksController::class, 'assetLinks']); Route::post('webhooks/plugins/{secret}', PluginWebhookController::class)->name('webhooks.plugins'); + +// Plugin purchase routes +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class), EnsureFeaturesAreActive::using(ShowPlugins::class)])->group(function () { + Route::get('plugins/{plugin}/purchase', [PluginPurchaseController::class, 'show'])->name('plugins.purchase.show'); + Route::post('plugins/{plugin}/purchase', [PluginPurchaseController::class, 'checkout'])->name('plugins.purchase.checkout'); + Route::get('plugins/{plugin}/purchase/success', [PluginPurchaseController::class, 'success'])->name('plugins.purchase.success'); + Route::get('plugins/{plugin}/purchase/status/{sessionId}', [PluginPurchaseController::class, 'status'])->name('plugins.purchase.status'); + Route::get('plugins/{plugin}/purchase/cancel', [PluginPurchaseController::class, 'cancel'])->name('plugins.purchase.cancel'); +}); + +// Bundle routes (public) +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function () { + Route::get('bundles/{bundle:slug}', [BundleController::class, 'show'])->name('bundles.show'); +}); + +// Cart routes (public - allows guest cart) +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function () { + Route::get('cart', [CartController::class, 'show'])->name('cart.show'); + Route::post('cart/add/{plugin}', [CartController::class, 'add'])->name('cart.add'); + Route::delete('cart/remove/{plugin}', [CartController::class, 'remove'])->name('cart.remove'); + Route::post('cart/bundle/{bundle:slug}', [CartController::class, 'addBundle'])->name('cart.bundle.add'); + Route::post('cart/bundle/{bundle:slug}/exchange', [CartController::class, 'exchangeForBundle'])->name('cart.bundle.exchange'); + Route::delete('cart/bundle/{bundle:slug}', [CartController::class, 'removeBundle'])->name('cart.bundle.remove'); + Route::delete('cart/clear', [CartController::class, 'clear'])->name('cart.clear'); + Route::get('cart/count', [CartController::class, 'count'])->name('cart.count'); + Route::post('cart/checkout', [CartController::class, 'checkout'])->name('cart.checkout'); + Route::get('cart/success', [CartController::class, 'success'])->name('cart.success')->middleware('auth'); + Route::get('cart/status/{sessionId}', [CartController::class, 'status'])->name('cart.status')->middleware('auth'); + Route::get('cart/cancel', [CartController::class, 'cancel'])->name('cart.cancel'); +}); + +// Developer onboarding routes +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class), EnsureFeaturesAreActive::using(ShowPlugins::class)])->prefix('customer/developer')->name('customer.developer.')->group(function () { + Route::get('onboarding', [DeveloperOnboardingController::class, 'show'])->name('onboarding'); + Route::post('onboarding/start', [DeveloperOnboardingController::class, 'start'])->name('onboarding.start'); + Route::get('onboarding/return', [DeveloperOnboardingController::class, 'return'])->name('onboarding.return'); + Route::get('onboarding/refresh', [DeveloperOnboardingController::class, 'refresh'])->name('onboarding.refresh'); + Route::get('dashboard', [DeveloperOnboardingController::class, 'dashboard'])->name('dashboard'); +}); + +// Plugin repository routes (plugins.nativephp.com subdomain) +Route::domain('plugins.'.parse_url(config('app.url'), PHP_URL_HOST))->group(function () { + Route::get('packages.json', [PluginRepositoryController::class, 'packagesJson'])->name('plugins.repository.packages'); + Route::get('p2/{vendor}/{package}.json', [PluginRepositoryController::class, 'packageMetadata']) + ->middleware(['composer.auth', 'plugin.access']) + ->name('plugins.repository.metadata'); + Route::get('dist/{vendor}/{package}/{version}.zip', [PluginRepositoryController::class, 'download']) + ->middleware(['composer.auth', 'plugin.access']) + ->name('plugins.repository.download'); +}); diff --git a/tests/Feature/Api/PluginAccessTest.php b/tests/Feature/Api/PluginAccessTest.php new file mode 100644 index 00000000..e248142e --- /dev/null +++ b/tests/Feature/Api/PluginAccessTest.php @@ -0,0 +1,207 @@ +getJson('/api/plugins/access'); + + $response->assertStatus(401) + ->assertJson(['error' => 'Authentication required']); + } + + public function test_returns_401_with_invalid_credentials(): void + { + $response = $this->asBasicAuth('invalid@example.com', 'invalid-key') + ->getJson('/api/plugins/access'); + + $response->assertStatus(401) + ->assertJson(['error' => 'Invalid credentials']); + } + + public function test_returns_accessible_plugins_with_valid_credentials(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + // Create a free plugin + $freePlugin = Plugin::factory()->create([ + 'name' => 'vendor/free-plugin', + 'type' => PluginType::Free, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + // Create a paid plugin + $paidPlugin = Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + // Give user a license for the paid plugin + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $paidPlugin->id, + 'expires_at' => null, // Never expires + ]); + + $response = $this->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'user' => ['email' => $user->email], + ]) + ->assertJsonCount(2, 'plugins'); + + $plugins = $response->json('plugins'); + $pluginNames = array_column($plugins, 'name'); + + $this->assertContains('vendor/free-plugin', $pluginNames); + $this->assertContains('vendor/paid-plugin', $pluginNames); + } + + public function test_excludes_expired_plugin_licenses(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + $paidPlugin = Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + // Create an expired license + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $paidPlugin->id, + 'expires_at' => now()->subDay(), + ]); + + $response = $this->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access'); + + $response->assertStatus(200); + + $plugins = $response->json('plugins'); + $pluginNames = array_column($plugins, 'name'); + + $this->assertNotContains('vendor/paid-plugin', $pluginNames); + } + + public function test_check_access_returns_true_for_licensed_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + $paidPlugin = Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $paidPlugin->id, + ]); + + $response = $this->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/paid-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'package' => 'vendor/paid-plugin', + 'has_access' => true, + ]); + } + + public function test_check_access_returns_false_for_unlicensed_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + $response = $this->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/paid-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'package' => 'vendor/paid-plugin', + 'has_access' => false, + ]); + } + + public function test_check_access_returns_true_for_free_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + Plugin::factory()->create([ + 'name' => 'vendor/free-plugin', + 'type' => PluginType::Free, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + $response = $this->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/free-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'package' => 'vendor/free-plugin', + 'has_access' => true, + ]); + } + + public function test_check_access_returns_404_for_nonexistent_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + $response = $this->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/nonexistent'); + + $response->assertStatus(404) + ->assertJson(['error' => 'Plugin not found']); + } + + protected function asBasicAuth(string $username, string $password): static + { + return $this->withHeaders([ + 'Authorization' => 'Basic '.base64_encode("{$username}:{$password}"), + ]); + } +} From 9f503eaed49e49d33d2abacc7aca0d2c0056d3b4 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 16 Jan 2026 20:41:02 +0000 Subject: [PATCH 31/34] Improve --- .../Resources/PluginBundleResource.php | 11 ++++++++++ .../Pages/ViewPluginBundle.php | 7 +++++++ app/Filament/Resources/PluginResource.php | 21 ------------------- .../views/customer/licenses/index.blade.php | 11 ++++++++-- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/app/Filament/Resources/PluginBundleResource.php b/app/Filament/Resources/PluginBundleResource.php index cb255ec7..c67fb6eb 100644 --- a/app/Filament/Resources/PluginBundleResource.php +++ b/app/Filament/Resources/PluginBundleResource.php @@ -174,6 +174,17 @@ public static function table(Table $table): Table ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), + Tables\Actions\ActionGroup::make([ + Tables\Actions\Action::make('viewListing') + ->label('View Listing Page') + ->icon('heroicon-o-eye') + ->color('gray') + ->url(fn (PluginBundle $record) => route('bundles.show', $record)) + ->openUrlInNewTab() + ->visible(fn (PluginBundle $record) => $record->is_active && $record->published_at?->isPast()), + ]) + ->label('More') + ->icon('heroicon-m-ellipsis-vertical'), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ diff --git a/app/Filament/Resources/PluginBundleResource/Pages/ViewPluginBundle.php b/app/Filament/Resources/PluginBundleResource/Pages/ViewPluginBundle.php index 8e75ade7..ff73bdd0 100644 --- a/app/Filament/Resources/PluginBundleResource/Pages/ViewPluginBundle.php +++ b/app/Filament/Resources/PluginBundleResource/Pages/ViewPluginBundle.php @@ -13,6 +13,13 @@ class ViewPluginBundle extends ViewRecord protected function getHeaderActions(): array { return [ + Actions\Action::make('viewListing') + ->label('View Listing Page') + ->icon('heroicon-o-eye') + ->color('gray') + ->url(fn () => route('bundles.show', $this->record)) + ->openUrlInNewTab() + ->visible(fn () => $this->record->is_active && $this->record->published_at?->isPast()), Actions\EditAction::make(), ]; } diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 98c9aa03..a1d9bcc9 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -211,26 +211,6 @@ public static function table(Table $table): Table ->openUrlInNewTab() ->visible(fn (Plugin $record) => $record->isFree()), - // Anystack Link (Paid plugins only) - Tables\Actions\Action::make('viewAnystack') - ->label('View on Anystack') - ->icon('heroicon-o-arrow-top-right-on-square') - ->color('gray') - ->url(fn (Plugin $record) => $record->getAnystackUrl()) - ->openUrlInNewTab() - ->visible(fn (Plugin $record) => $record->isPaid() && $record->anystack_id), - - // Anystack Instructions (Paid plugins without ID) - Tables\Actions\Action::make('anystackInstructions') - ->label('Anystack Setup') - ->icon('heroicon-o-information-circle') - ->color('warning') - ->visible(fn (Plugin $record) => $record->isPaid()) - ->modalHeading('Anystack Verification Required') - ->modalDescription('For paid plugins, verify that the developer has applied to the "NativePHP Plugin Directory" affiliate program in their Anystack dashboard under the Advertising section.') - ->modalSubmitAction(false) - ->modalCancelActionLabel('Close'), - // Edit Description Action Tables\Actions\Action::make('editDescription') ->label('Edit Description') @@ -250,7 +230,6 @@ public static function table(Table $table): Table ->modalDescription(fn (Plugin $record) => "Update the description for '{$record->name}'"), Tables\Actions\ViewAction::make(), - Tables\Actions\DeleteAction::make(), ]) ->label('More') ->icon('heroicon-m-ellipsis-vertical'), diff --git a/resources/views/customer/licenses/index.blade.php b/resources/views/customer/licenses/index.blade.php index fa2ab236..1210c25d 100644 --- a/resources/views/customer/licenses/index.blade.php +++ b/resources/views/customer/licenses/index.blade.php @@ -85,8 +85,15 @@ class="rounded bg-indigo-600 px-2.5 py-1.5 text-xs font-medium text-white hover: How to configure Composer -
- composer config http-basic.plugins.nativephp.com {{ auth()->user()->email }} {{ auth()->user()->plugin_license_key }} +
+

1. Add the NativePHP plugins repository:

+
+ composer config repositories.nativephp-plugins composer https://plugins.nativephp.com +
+

2. Configure your credentials:

+
+ composer config http-basic.plugins.nativephp.com {{ auth()->user()->email }} {{ auth()->user()->plugin_license_key }} +
From c01b9c192e90fa9fd81fa5036a2adf8d46976f0f Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sat, 17 Jan 2026 13:22:56 +0000 Subject: [PATCH 32/34] Chart --- app/Filament/Pages/Dashboard.php | 2 + .../Widgets/LicenseDistributionChart.php | 22 ++-- app/Filament/Widgets/PluginRevenueChart.php | 115 ++++++++++++++++++ 3 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 app/Filament/Widgets/PluginRevenueChart.php diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index c88539c9..8961642d 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -4,6 +4,7 @@ use App\Filament\Widgets\LicenseDistributionChart; use App\Filament\Widgets\LicensesChart; +use App\Filament\Widgets\PluginRevenueChart; use App\Filament\Widgets\StatsOverview; use App\Filament\Widgets\UsersChart; use Filament\Pages\Dashboard as BaseDashboard; @@ -25,6 +26,7 @@ public function getWidgets(): array UsersChart::class, LicensesChart::class, LicenseDistributionChart::class, + PluginRevenueChart::class, ]; } } diff --git a/app/Filament/Widgets/LicenseDistributionChart.php b/app/Filament/Widgets/LicenseDistributionChart.php index b1aa1307..10eaa35e 100644 --- a/app/Filament/Widgets/LicenseDistributionChart.php +++ b/app/Filament/Widgets/LicenseDistributionChart.php @@ -70,24 +70,20 @@ protected function getLicenseDistribution(): array protected function getOptions(): array { return [ + 'scales' => [ + 'x' => [ + 'display' => false, + ], + 'y' => [ + 'display' => false, + ], + ], 'plugins' => [ 'legend' => [ 'position' => 'bottom', ], 'tooltip' => [ - 'callbacks' => [ - 'label' => '/** - * @param {Object} context - * @returns {string} - */ - function(context) { - const label = context.label || ""; - const value = context.raw || 0; - const total = context.chart.data.datasets[0].data.reduce((a, b) => a + b, 0); - const percentage = Math.round((value / total) * 100); - return `${label}: ${value} (${percentage}%)`; - }', - ], + 'enabled' => true, ], ], ]; diff --git a/app/Filament/Widgets/PluginRevenueChart.php b/app/Filament/Widgets/PluginRevenueChart.php new file mode 100644 index 00000000..3342c78e --- /dev/null +++ b/app/Filament/Widgets/PluginRevenueChart.php @@ -0,0 +1,115 @@ +getRevenueByPlugin(); + + return [ + 'datasets' => [ + [ + 'label' => 'Revenue', + 'data' => $data['amounts'], + 'backgroundColor' => [ + 'rgba(59, 130, 246, 0.7)', // blue + 'rgba(16, 185, 129, 0.7)', // green + 'rgba(249, 115, 22, 0.7)', // orange + 'rgba(139, 92, 246, 0.7)', // purple + 'rgba(236, 72, 153, 0.7)', // pink + 'rgba(245, 158, 11, 0.7)', // amber + 'rgba(20, 184, 166, 0.7)', // teal + 'rgba(239, 68, 68, 0.7)', // red + 'rgba(99, 102, 241, 0.7)', // indigo + 'rgba(168, 162, 158, 0.7)', // stone + ], + 'borderColor' => [ + 'rgb(59, 130, 246)', + 'rgb(16, 185, 129)', + 'rgb(249, 115, 22)', + 'rgb(139, 92, 246)', + 'rgb(236, 72, 153)', + 'rgb(245, 158, 11)', + 'rgb(20, 184, 166)', + 'rgb(239, 68, 68)', + 'rgb(99, 102, 241)', + 'rgb(168, 162, 158)', + ], + 'borderWidth' => 1, + ], + ], + 'labels' => $data['labels'], + ]; + } + + protected function getType(): string + { + return 'pie'; + } + + public function getDescription(): ?string + { + $total = PluginLicense::sum('price_paid'); + + return 'Top 10 plugins by revenue. Total: $'.number_format($total / 100, 2); + } + + protected function getRevenueByPlugin(): array + { + $revenues = PluginLicense::select('plugin_id', DB::raw('SUM(price_paid) as total_revenue')) + ->whereNotNull('plugin_id') + ->groupBy('plugin_id') + ->orderByDesc('total_revenue') + ->limit(10) + ->with('plugin:id,name') + ->get(); + + $labels = []; + $amounts = []; + + foreach ($revenues as $revenue) { + $pluginName = $revenue->plugin?->name ?? 'Unknown'; + $labels[] = $pluginName; + $amounts[] = $revenue->total_revenue / 100; // Convert cents to dollars for display + } + + return [ + 'labels' => $labels, + 'amounts' => $amounts, + ]; + } + + protected function getOptions(): array + { + return [ + 'scales' => [ + 'x' => [ + 'display' => false, + ], + 'y' => [ + 'display' => false, + ], + ], + 'plugins' => [ + 'legend' => [ + 'position' => 'bottom', + ], + 'tooltip' => [ + 'enabled' => true, + ], + ], + ]; + } +} From 46bfcf2507f385a11a145c14d42aeccfb19d93b6 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sat, 17 Jan 2026 14:46:19 +0000 Subject: [PATCH 33/34] Fix tests, remove unused functionality --- .../PluginRepositoryController.php | 83 --------- app/Http/Kernel.php | 2 - app/Http/Middleware/ComposerBasicAuth.php | 55 ------ app/Http/Middleware/PluginAccessCheck.php | 91 ---------- app/Jobs/BuildPluginPackage.php | 157 ------------------ app/Jobs/SyncPluginReleases.php | 18 +- app/Models/Plugin.php | 16 -- app/Providers/RouteServiceProvider.php | 2 +- app/Services/PluginStorageService.php | 143 ---------------- app/Services/SatisGeneratorService.php | 144 ---------------- config/filesystems.php | 12 -- database/factories/PluginFactory.php | 3 - ...nd_anystack_columns_from_plugins_table.php | 32 ++++ routes/web.php | 12 -- tests/Feature/Api/PluginAccessTest.php | 45 +++-- tests/Feature/CustomerAuthenticationTest.php | 6 +- .../Feature/CustomerLicenseManagementTest.php | 12 +- 17 files changed, 88 insertions(+), 745 deletions(-) delete mode 100644 app/Http/Controllers/PluginRepositoryController.php delete mode 100644 app/Http/Middleware/ComposerBasicAuth.php delete mode 100644 app/Http/Middleware/PluginAccessCheck.php delete mode 100644 app/Jobs/BuildPluginPackage.php delete mode 100644 app/Services/PluginStorageService.php delete mode 100644 app/Services/SatisGeneratorService.php create mode 100644 database/migrations/2026_01_17_143327_remove_unused_satis_and_anystack_columns_from_plugins_table.php diff --git a/app/Http/Controllers/PluginRepositoryController.php b/app/Http/Controllers/PluginRepositoryController.php deleted file mode 100644 index 08203734..00000000 --- a/app/Http/Controllers/PluginRepositoryController.php +++ /dev/null @@ -1,83 +0,0 @@ -storageService->getPackagesJsonContent(); - - if (! $content) { - return response('Repository not found', 404); - } - - return response($content, 200, [ - 'Content-Type' => 'application/json', - ]); - } - - public function packageMetadata(Request $request, string $vendor, string $package): Response - { - $content = $this->storageService->getPackageMetadataContent($vendor, $package); - - if (! $content) { - return response('Package not found', 404); - } - - return response($content, 200, [ - 'Content-Type' => 'application/json', - ]); - } - - public function download(Request $request, string $vendor, string $package, string $version): Response - { - $plugin = $request->attributes->get('plugin'); - - if (! $plugin instanceof Plugin) { - $pluginName = "{$vendor}/{$package}"; - $plugin = Plugin::where('name', $pluginName)->approved()->first(); - - if (! $plugin) { - return response('Package not found', 404); - } - } - - $version = str_replace('.zip', '', $version); - - if (! $this->storageService->packageExists($plugin, $version)) { - return response('Version not found', 404); - } - - try { - $signedUrl = $this->storageService->generateSignedUrl($plugin, $version); - - Log::info('Plugin package download', [ - 'plugin_id' => $plugin->id, - 'plugin_name' => $plugin->name, - 'version' => $version, - 'user_id' => $request->user()?->id, - ]); - - return response('', 302, [ - 'Location' => $signedUrl, - ]); - } catch (\Exception $e) { - Log::error('Failed to generate signed URL for plugin download', [ - 'plugin_id' => $plugin->id, - 'version' => $version, - 'error' => $e->getMessage(), - ]); - - return response('Download failed', 500); - } - } -} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 364ed32a..4e74a273 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -59,10 +59,8 @@ class Kernel extends HttpKernel 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'composer.auth' => \App\Http\Middleware\ComposerBasicAuth::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'plugin.access' => \App\Http\Middleware\PluginAccessCheck::class, 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, diff --git a/app/Http/Middleware/ComposerBasicAuth.php b/app/Http/Middleware/ComposerBasicAuth.php deleted file mode 100644 index 8f3aace9..00000000 --- a/app/Http/Middleware/ComposerBasicAuth.php +++ /dev/null @@ -1,55 +0,0 @@ -getUser(); - $licenseKey = $request->getPassword(); - - if (! $email || ! $licenseKey) { - return $this->unauthorized('Authentication required'); - } - - $user = User::where('email', $email) - ->where('plugin_license_key', $licenseKey) - ->first(); - - if (! $user) { - Log::info('Plugin repository auth failed', [ - 'email' => $email, - 'ip' => $request->ip(), - 'user_agent' => $request->userAgent(), - ]); - - return $this->unauthorized('Invalid credentials'); - } - - $request->setUserResolver(fn () => $user); - - return $next($request); - } - - protected function unauthorized(string $message): Response - { - return response($message, 401, [ - 'WWW-Authenticate' => 'Basic realm="NativePHP Plugin Repository"', - ]); - } -} diff --git a/app/Http/Middleware/PluginAccessCheck.php b/app/Http/Middleware/PluginAccessCheck.php deleted file mode 100644 index bd78c8e8..00000000 --- a/app/Http/Middleware/PluginAccessCheck.php +++ /dev/null @@ -1,91 +0,0 @@ -user(); - - if (! $user instanceof User) { - return response('Authentication required', 401); - } - - $plugin = $this->resolvePlugin($request); - - if (! $plugin) { - return response('Plugin not found', 404); - } - - if (! $this->userHasAccess($user, $plugin)) { - return response('You do not have access to this plugin', 403); - } - - $request->attributes->set('plugin', $plugin); - - return $next($request); - } - - protected function resolvePlugin(Request $request): ?Plugin - { - if ($request->route('plugin') instanceof Plugin) { - return $request->route('plugin'); - } - - $vendor = $request->route('vendor'); - $package = $request->route('package'); - - if ($vendor && $package) { - $packageName = "{$vendor}/{$package}"; - - return Plugin::where('name', $packageName)->approved()->first(); - } - - return null; - } - - protected function userHasAccess(User $user, Plugin $plugin): bool - { - if ($plugin->type === PluginType::Free) { - return true; - } - - if ($user->pluginLicenses()->forPlugin($plugin)->active()->exists()) { - return true; - } - - if ($plugin->is_official && $this->userHasGrandfatheredAccess($user)) { - return true; - } - - return false; - } - - protected function userHasGrandfatheredAccess(User $user): bool - { - $purchaseHistory = $user->purchaseHistory; - - if (! $purchaseHistory) { - return false; - } - - return $purchaseHistory->grandfathering_tier === GrandfatheringTier::FreeOfficialPlugins; - } -} diff --git a/app/Jobs/BuildPluginPackage.php b/app/Jobs/BuildPluginPackage.php deleted file mode 100644 index b545c907..00000000 --- a/app/Jobs/BuildPluginPackage.php +++ /dev/null @@ -1,157 +0,0 @@ -pluginVersion->plugin; - $repo = $plugin->getRepositoryOwnerAndName(); - - if (! $repo) { - Log::warning("Plugin {$plugin->id} has no valid repository URL"); - - return; - } - - $zipPath = $this->downloadReleaseArchive($repo['owner'], $repo['repo']); - - if (! $zipPath) { - Log::error('Failed to download release archive', [ - 'plugin_id' => $plugin->id, - 'version' => $this->pluginVersion->version, - ]); - - return; - } - - try { - $storagePath = $storageService->uploadPackage( - $plugin, - $this->pluginVersion->version, - $zipPath - ); - - $fileSize = filesize($zipPath); - - $this->pluginVersion->update([ - 'storage_path' => $storagePath, - 'file_size' => $fileSize, - 'is_packaged' => true, - 'packaged_at' => now(), - ]); - - Log::info('Plugin package built and uploaded', [ - 'plugin_id' => $plugin->id, - 'version' => $this->pluginVersion->version, - 'storage_path' => $storagePath, - ]); - - $this->triggerSatisBuild(); - } finally { - @unlink($zipPath); - } - } - - protected function downloadReleaseArchive(string $owner, string $repo): ?string - { - $token = $this->getGitHubToken(); - $tagName = $this->pluginVersion->tag_name; - - $archiveUrl = "https://api.github.com/repos/{$owner}/{$repo}/zipball/{$tagName}"; - - $request = Http::timeout(120)->withOptions([ - 'stream' => true, - ]); - - if ($token) { - $request = $request->withToken($token); - } - - $response = $request->get($archiveUrl); - - if ($response->failed()) { - Log::warning('Failed to download release archive', [ - 'url' => $archiveUrl, - 'status' => $response->status(), - ]); - - return null; - } - - $tempPath = storage_path('app/temp/plugin-'.uniqid().'.zip'); - - if (! is_dir(dirname($tempPath))) { - mkdir(dirname($tempPath), 0755, true); - } - - file_put_contents($tempPath, $response->body()); - - return $tempPath; - } - - protected function triggerSatisBuild(): void - { - $satisUrl = config('services.satis.url'); - $satisApiKey = config('services.satis.api_key'); - - if (! $satisUrl || ! $satisApiKey) { - Log::info('Satis not configured, skipping build trigger'); - - return; - } - - try { - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$satisApiKey, - ]) - ->timeout(30) - ->post("{$satisUrl}/api/build"); - - if ($response->failed()) { - Log::warning('Failed to trigger Satis build', [ - 'status' => $response->status(), - 'response' => $response->json(), - ]); - } - } catch (\Exception $e) { - Log::warning('Failed to trigger Satis build', [ - 'error' => $e->getMessage(), - ]); - } - } - - protected function getGitHubToken(): ?string - { - $user = $this->pluginVersion->plugin->user; - - if ($user && $user->hasGitHubToken()) { - return $user->getGitHubToken(); - } - - return config('services.github.token'); - } -} diff --git a/app/Jobs/SyncPluginReleases.php b/app/Jobs/SyncPluginReleases.php index f2481ff0..edbb176a 100644 --- a/app/Jobs/SyncPluginReleases.php +++ b/app/Jobs/SyncPluginReleases.php @@ -4,6 +4,7 @@ use App\Models\Plugin; use App\Models\PluginVersion; +use App\Services\SatisService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -20,12 +21,14 @@ class SyncPluginReleases implements ShouldQueue public int $backoff = 60; + protected bool $hasNewReleases = false; + public function __construct( public Plugin $plugin, - public bool $buildNewReleases = true + public bool $triggerSatisBuild = true ) {} - public function handle(): void + public function handle(SatisService $satisService): void { if (! $this->plugin->isApproved()) { Log::info("Plugin {$this->plugin->id} is not approved, skipping release sync"); @@ -50,6 +53,11 @@ public function handle(): void } $this->plugin->update(['last_synced_at' => now()]); + + // Trigger satis build if we have new releases + if ($this->triggerSatisBuild && $this->hasNewReleases) { + $satisService->build([$this->plugin]); + } } protected function fetchReleases(string $owner, string $repo, ?string $token): array @@ -89,7 +97,7 @@ protected function processRelease(array $release): void return; } - $pluginVersion = PluginVersion::create([ + PluginVersion::create([ 'plugin_id' => $this->plugin->id, 'version' => $version, 'tag_name' => $tagName, @@ -104,9 +112,7 @@ protected function processRelease(array $release): void 'version' => $version, ]); - if ($this->buildNewReleases && $this->plugin->isPaid()) { - BuildPluginPackage::dispatch($pluginVersion); - } + $this->hasNewReleases = true; } protected function getGitHubToken(): ?string diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index b897e0e9..ba851e79 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -30,7 +30,6 @@ class Plugin extends Model 'featured' => 'boolean', 'is_active' => 'boolean', 'is_official' => 'boolean', - 'satis_included' => 'boolean', 'composer_data' => 'array', 'nativephp_data' => 'array', 'last_synced_at' => 'datetime', @@ -176,11 +175,6 @@ public function isOfficial(): bool return $this->is_official ?? false; } - public function isIncludedInSatis(): bool - { - return $this->satis_included ?? false; - } - /** * @param Builder $query * @return Builder @@ -219,15 +213,6 @@ public function getGithubUrl(): string return "https://github.com/{$this->name}"; } - public function getAnystackUrl(): ?string - { - if (! $this->anystack_id) { - return null; - } - - return "https://anystack.sh/products/{$this->anystack_id}"; - } - public function getWebhookUrl(): ?string { if (! $this->webhook_secret) { @@ -367,7 +352,6 @@ public function approve(int $approvedById): void 'approved_at' => now(), 'approved_by' => $approvedById, 'rejection_reason' => null, - 'satis_included' => $this->isPaid(), ]); $this->recordActivity( diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 091a8f97..b48530e3 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider * * @var string */ - public const HOME = '/customer/licenses'; + public const HOME = '/dashboard'; /** * Define your route model bindings, pattern filters, and other route configuration. diff --git a/app/Services/PluginStorageService.php b/app/Services/PluginStorageService.php deleted file mode 100644 index 08ab8d5e..00000000 --- a/app/Services/PluginStorageService.php +++ /dev/null @@ -1,143 +0,0 @@ -getPackagePath($plugin, $version); - - Storage::disk($this->disk)->put($storagePath, file_get_contents($zipPath)); - - Log::info('Uploaded plugin package', [ - 'plugin_id' => $plugin->id, - 'plugin_name' => $plugin->name, - 'version' => $version, - 'path' => $storagePath, - ]); - - return $storagePath; - } - - public function uploadPackagesJson(string $content): string - { - $path = 'packages.json'; - - Storage::disk($this->disk)->put($path, $content); - - Log::info('Uploaded packages.json'); - - return $path; - } - - public function uploadPackageMetadata(Plugin $plugin, string $content): string - { - [$vendor, $package] = explode('/', $plugin->name); - - $path = "p2/{$vendor}/{$package}.json"; - - Storage::disk($this->disk)->put($path, $content); - - return $path; - } - - public function getPackageUrl(Plugin $plugin, string $version): string - { - $path = $this->getPackagePath($plugin, $version); - - return Storage::disk($this->disk)->url($path); - } - - public function generateSignedUrl(Plugin $plugin, string $version, int $expirationMinutes = 15): string - { - $path = $this->getPackagePath($plugin, $version); - - return Storage::disk($this->disk)->temporaryUrl( - $path, - now()->addMinutes($expirationMinutes) - ); - } - - public function deletePackage(Plugin $plugin, ?string $version = null): bool - { - if ($version) { - $path = $this->getPackagePath($plugin, $version); - - return Storage::disk($this->disk)->delete($path); - } - - [$vendor, $package] = explode('/', $plugin->name); - $directory = "dist/{$vendor}/{$package}"; - - return Storage::disk($this->disk)->deleteDirectory($directory); - } - - public function packageExists(Plugin $plugin, string $version): bool - { - $path = $this->getPackagePath($plugin, $version); - - return Storage::disk($this->disk)->exists($path); - } - - public function getPackageSize(Plugin $plugin, string $version): ?int - { - $path = $this->getPackagePath($plugin, $version); - - if (! Storage::disk($this->disk)->exists($path)) { - return null; - } - - return Storage::disk($this->disk)->size($path); - } - - public function listPackageVersions(Plugin $plugin): array - { - [$vendor, $package] = explode('/', $plugin->name); - $directory = "dist/{$vendor}/{$package}"; - - $files = Storage::disk($this->disk)->files($directory); - - $versions = []; - foreach ($files as $file) { - if (preg_match('/\/([^\/]+)\.zip$/', $file, $matches)) { - $versions[] = $matches[1]; - } - } - - return $versions; - } - - protected function getPackagePath(Plugin $plugin, string $version): string - { - [$vendor, $package] = explode('/', $plugin->name); - - return "dist/{$vendor}/{$package}/{$version}.zip"; - } - - public function getPackagesJsonContent(): ?string - { - if (! Storage::disk($this->disk)->exists('packages.json')) { - return null; - } - - return Storage::disk($this->disk)->get('packages.json'); - } - - public function getPackageMetadataContent(string $vendor, string $package): ?string - { - $path = "p2/{$vendor}/{$package}.json"; - - if (! Storage::disk($this->disk)->exists($path)) { - return null; - } - - return Storage::disk($this->disk)->get($path); - } -} diff --git a/app/Services/SatisGeneratorService.php b/app/Services/SatisGeneratorService.php deleted file mode 100644 index cb7afcbf..00000000 --- a/app/Services/SatisGeneratorService.php +++ /dev/null @@ -1,144 +0,0 @@ -getPluginsForSatis($specificPlugin); - - if ($plugins->isEmpty()) { - Log::info('No plugins to include in Satis repository'); - - return true; - } - - return $this->triggerSatisBuild($plugins); - } - - public function rebuildAll(): bool - { - return $this->generate(); - } - - public function rebuildPlugin(Plugin $plugin): bool - { - return $this->generate($plugin); - } - - /** - * @return Collection - */ - protected function getPluginsForSatis(?Plugin $specificPlugin = null): Collection - { - $query = Plugin::query() - ->approved() - ->where('satis_included', true); - - if ($specificPlugin) { - $query->where('id', $specificPlugin->id); - } - - return $query->get(); - } - - /** - * @param Collection $plugins - */ - protected function triggerSatisBuild(Collection $plugins): bool - { - $satisApiUrl = config('services.satis.url'); - $satisApiKey = config('services.satis.api_key'); - - if (! $satisApiUrl || ! $satisApiKey) { - Log::error('Satis API not configured', [ - 'url_set' => ! empty($satisApiUrl), - 'key_set' => ! empty($satisApiKey), - ]); - - return false; - } - - $payload = [ - 'plugins' => $plugins->map(function (Plugin $plugin) { - return [ - 'name' => $plugin->name, - 'repository_url' => $plugin->repository_url, - 'type' => $plugin->type->value, - 'is_official' => $plugin->is_official, - ]; - })->values()->all(), - ]; - - try { - $response = Http::timeout(30) - ->withToken($satisApiKey) - ->post("{$satisApiUrl}/api/build", $payload); - - if ($response->successful()) { - Log::info('Satis build triggered successfully', [ - 'plugins_count' => $plugins->count(), - 'job_id' => $response->json('job_id'), - ]); - - return true; - } - - Log::error('Satis API returned error', [ - 'status' => $response->status(), - 'body' => $response->body(), - ]); - - return false; - } catch (\Exception $e) { - Log::error('Failed to trigger Satis build', [ - 'error' => $e->getMessage(), - ]); - - return false; - } - } - - public function markPluginForInclusion(Plugin $plugin): void - { - $plugin->update(['satis_included' => true]); - } - - public function removePluginFromInclusion(Plugin $plugin): void - { - $plugin->update(['satis_included' => false]); - - $this->triggerPluginRemoval($plugin); - } - - protected function triggerPluginRemoval(Plugin $plugin): bool - { - $satisApiUrl = config('services.satis.url'); - $satisApiKey = config('services.satis.api_key'); - - if (! $satisApiUrl || ! $satisApiKey) { - return false; - } - - try { - $response = Http::timeout(30) - ->withToken($satisApiKey) - ->delete("{$satisApiUrl}/api/packages/{$plugin->name}"); - - return $response->successful(); - } catch (\Exception $e) { - Log::error('Failed to trigger plugin removal from Satis', [ - 'plugin' => $plugin->name, - 'error' => $e->getMessage(), - ]); - - return false; - } - } -} diff --git a/config/filesystems.php b/config/filesystems.php index 0c1c6af7..e9d9dbdb 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -56,18 +56,6 @@ 'throw' => false, ], - 'r2_plugins' => [ - 'driver' => 's3', - 'key' => env('R2_ACCESS_KEY_ID'), - 'secret' => env('R2_SECRET_ACCESS_KEY'), - 'region' => 'auto', - 'bucket' => env('R2_PLUGINS_BUCKET', 'nativephp-plugins'), - 'url' => env('R2_PLUGINS_URL'), - 'endpoint' => env('R2_ENDPOINT'), - 'use_path_style_endpoint' => false, - 'throw' => true, - ], - ], /* diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php index d67364a8..9c2861a5 100644 --- a/database/factories/PluginFactory.php +++ b/database/factories/PluginFactory.php @@ -112,7 +112,6 @@ public function definition(): array 'type' => PluginType::Free, 'status' => PluginStatus::Pending, 'featured' => false, - 'anystack_id' => null, 'rejection_reason' => null, 'approved_at' => null, 'approved_by' => null, @@ -169,7 +168,6 @@ public function free(): static { return $this->state(fn (array $attributes) => [ 'type' => PluginType::Free, - 'anystack_id' => null, ]); } @@ -177,7 +175,6 @@ public function paid(): static { return $this->state(fn (array $attributes) => [ 'type' => PluginType::Paid, - 'anystack_id' => fake()->uuid(), ]); } diff --git a/database/migrations/2026_01_17_143327_remove_unused_satis_and_anystack_columns_from_plugins_table.php b/database/migrations/2026_01_17_143327_remove_unused_satis_and_anystack_columns_from_plugins_table.php new file mode 100644 index 00000000..4c5c159e --- /dev/null +++ b/database/migrations/2026_01_17_143327_remove_unused_satis_and_anystack_columns_from_plugins_table.php @@ -0,0 +1,32 @@ +dropIndex(['satis_included']); + $table->dropColumn(['satis_included', 'anystack_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->boolean('satis_included')->default(false)->after('is_official'); + $table->string('anystack_id')->nullable()->after('type'); + + $table->index('satis_included'); + }); + } +}; diff --git a/routes/web.php b/routes/web.php index 309605a6..691bfe27 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,7 +13,6 @@ use App\Http\Controllers\OpenCollectiveWebhookController; use App\Http\Controllers\PluginDirectoryController; use App\Http\Controllers\PluginPurchaseController; -use App\Http\Controllers\PluginRepositoryController; use App\Http\Controllers\PluginWebhookController; use App\Http\Controllers\ShowBlogController; use App\Http\Controllers\ShowDocumentationController; @@ -296,14 +295,3 @@ Route::get('onboarding/refresh', [DeveloperOnboardingController::class, 'refresh'])->name('onboarding.refresh'); Route::get('dashboard', [DeveloperOnboardingController::class, 'dashboard'])->name('dashboard'); }); - -// Plugin repository routes (plugins.nativephp.com subdomain) -Route::domain('plugins.'.parse_url(config('app.url'), PHP_URL_HOST))->group(function () { - Route::get('packages.json', [PluginRepositoryController::class, 'packagesJson'])->name('plugins.repository.packages'); - Route::get('p2/{vendor}/{package}.json', [PluginRepositoryController::class, 'packageMetadata']) - ->middleware(['composer.auth', 'plugin.access']) - ->name('plugins.repository.metadata'); - Route::get('dist/{vendor}/{package}/{version}.zip', [PluginRepositoryController::class, 'download']) - ->middleware(['composer.auth', 'plugin.access']) - ->name('plugins.repository.download'); -}); diff --git a/tests/Feature/Api/PluginAccessTest.php b/tests/Feature/Api/PluginAccessTest.php index e248142e..cae1b7b8 100644 --- a/tests/Feature/Api/PluginAccessTest.php +++ b/tests/Feature/Api/PluginAccessTest.php @@ -14,17 +14,27 @@ class PluginAccessTest extends TestCase { use RefreshDatabase; - public function test_returns_401_without_credentials(): void + public function test_returns_401_without_api_key(): void { $response = $this->getJson('/api/plugins/access'); + $response->assertStatus(401) + ->assertJson(['message' => 'Unauthorized']); + } + + public function test_returns_401_without_credentials(): void + { + $response = $this->withApiKey() + ->getJson('/api/plugins/access'); + $response->assertStatus(401) ->assertJson(['error' => 'Authentication required']); } public function test_returns_401_with_invalid_credentials(): void { - $response = $this->asBasicAuth('invalid@example.com', 'invalid-key') + $response = $this->withApiKey() + ->asBasicAuth('invalid@example.com', 'invalid-key') ->getJson('/api/plugins/access'); $response->assertStatus(401) @@ -60,20 +70,21 @@ public function test_returns_accessible_plugins_with_valid_credentials(): void 'expires_at' => null, // Never expires ]); - $response = $this->asBasicAuth($user->email, 'test-license-key-123') + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') ->getJson('/api/plugins/access'); $response->assertStatus(200) ->assertJson([ 'success' => true, 'user' => ['email' => $user->email], - ]) - ->assertJsonCount(2, 'plugins'); + ]); $plugins = $response->json('plugins'); $pluginNames = array_column($plugins, 'name'); - $this->assertContains('vendor/free-plugin', $pluginNames); + // Only paid plugins with licenses are returned (not free plugins) + $this->assertNotContains('vendor/free-plugin', $pluginNames); $this->assertContains('vendor/paid-plugin', $pluginNames); } @@ -97,7 +108,8 @@ public function test_excludes_expired_plugin_licenses(): void 'expires_at' => now()->subDay(), ]); - $response = $this->asBasicAuth($user->email, 'test-license-key-123') + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') ->getJson('/api/plugins/access'); $response->assertStatus(200); @@ -126,7 +138,8 @@ public function test_check_access_returns_true_for_licensed_plugin(): void 'plugin_id' => $paidPlugin->id, ]); - $response = $this->asBasicAuth($user->email, 'test-license-key-123') + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') ->getJson('/api/plugins/access/vendor/paid-plugin'); $response->assertStatus(200) @@ -150,7 +163,8 @@ public function test_check_access_returns_false_for_unlicensed_plugin(): void 'is_active' => true, ]); - $response = $this->asBasicAuth($user->email, 'test-license-key-123') + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') ->getJson('/api/plugins/access/vendor/paid-plugin'); $response->assertStatus(200) @@ -174,7 +188,8 @@ public function test_check_access_returns_true_for_free_plugin(): void 'is_active' => true, ]); - $response = $this->asBasicAuth($user->email, 'test-license-key-123') + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') ->getJson('/api/plugins/access/vendor/free-plugin'); $response->assertStatus(200) @@ -191,7 +206,8 @@ public function test_check_access_returns_404_for_nonexistent_plugin(): void 'plugin_license_key' => 'test-license-key-123', ]); - $response = $this->asBasicAuth($user->email, 'test-license-key-123') + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') ->getJson('/api/plugins/access/vendor/nonexistent'); $response->assertStatus(404) @@ -204,4 +220,11 @@ protected function asBasicAuth(string $username, string $password): static 'Authorization' => 'Basic '.base64_encode("{$username}:{$password}"), ]); } + + protected function withApiKey(): static + { + return $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + ]); + } } diff --git a/tests/Feature/CustomerAuthenticationTest.php b/tests/Feature/CustomerAuthenticationTest.php index d849388e..2673937c 100644 --- a/tests/Feature/CustomerAuthenticationTest.php +++ b/tests/Feature/CustomerAuthenticationTest.php @@ -42,7 +42,7 @@ public function test_customer_can_login_with_valid_credentials(): void 'password' => 'password', ]); - $response->assertRedirect('/customer/licenses'); + $response->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); } @@ -86,12 +86,12 @@ public function test_authenticated_customer_is_redirected_from_login_page(): voi $response = $this->actingAs($user)->get('/login'); - $response->assertRedirect('/customer/licenses'); + $response->assertRedirect('/dashboard'); } public function test_unauthenticated_customer_is_redirected_to_login(): void { - $response = $this->get('/customer/licenses'); + $response = $this->get('/dashboard'); $response->assertRedirect('/login'); } diff --git a/tests/Feature/CustomerLicenseManagementTest.php b/tests/Feature/CustomerLicenseManagementTest.php index ba266744..e245255f 100644 --- a/tests/Feature/CustomerLicenseManagementTest.php +++ b/tests/Feature/CustomerLicenseManagementTest.php @@ -25,7 +25,7 @@ public function test_customer_can_view_licenses_page(): void { $user = User::factory()->create(); - $response = $this->actingAs($user)->get('/customer/licenses'); + $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); $response->assertSee('Your Licenses'); @@ -36,7 +36,7 @@ public function test_customer_sees_no_licenses_message_when_no_licenses_exist(): { $user = User::factory()->create(); - $response = $this->actingAs($user)->get('/customer/licenses'); + $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); $response->assertSee('No licenses found'); @@ -57,7 +57,7 @@ public function test_customer_can_view_their_licenses(): void 'key' => 'test-key-2', ]); - $response = $this->actingAs($user)->get('/customer/licenses'); + $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); $response->assertSee('Standard License'); @@ -80,7 +80,7 @@ public function test_customer_cannot_view_other_customers_licenses(): void 'policy_name' => 'User 2 License', ]); - $response = $this->actingAs($user1)->get('/customer/licenses'); + $response = $this->actingAs($user1)->get('/dashboard'); $response->assertStatus(200); $response->assertSee('User 1 License'); @@ -145,7 +145,7 @@ public function test_license_status_displays_correctly(): void 'is_suspended' => true, ]); - $response = $this->actingAs($user)->get('/customer/licenses'); + $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); $response->assertSee('Active'); @@ -249,7 +249,7 @@ public function test_license_names_display_on_index_page(): void 'name' => null, ]); - $response = $this->actingAs($user)->get('/customer/licenses'); + $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); // Named license should show custom name prominently From 701d2ee0d7f2062dbef475cb16941074ce05ed41 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sun, 18 Jan 2026 17:14:30 +0000 Subject: [PATCH 34/34] Bump deps --- composer.lock | 895 +++++++++++++++++++++++++------------------------- 1 file changed, 453 insertions(+), 442 deletions(-) diff --git a/composer.lock b/composer.lock index b11e124c..5fea3c3d 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "anourvalar/eloquent-serialize", - "version": "1.3.4", + "version": "1.3.5", "source": { "type": "git", "url": "https://github.com/AnourValar/eloquent-serialize.git", - "reference": "0934a98866e02b73e38696961a9d7984b834c9d9" + "reference": "1a7dead8d532657e5358f8f27c0349373517681e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/0934a98866e02b73e38696961a9d7984b834c9d9", - "reference": "0934a98866e02b73e38696961a9d7984b834c9d9", + "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/1a7dead8d532657e5358f8f27c0349373517681e", + "reference": "1a7dead8d532657e5358f8f27c0349373517681e", "shasum": "" }, "require": { @@ -68,9 +68,9 @@ ], "support": { "issues": "https://github.com/AnourValar/eloquent-serialize/issues", - "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.4" + "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.5" }, - "time": "2025-07-30T15:45:57+00:00" + "time": "2025-12-04T13:38:21+00:00" }, { "name": "artesaos/seotools", @@ -199,16 +199,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.12", + "version": "3.369.15", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "36ee8894743a254ae2650bad4968c874b76bc7de" + "reference": "7c62f41fb0460c3e5d5c1f70e93e726f1daa75f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/36ee8894743a254ae2650bad4968c874b76bc7de", - "reference": "36ee8894743a254ae2650bad4968c874b76bc7de", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7c62f41fb0460c3e5d5c1f70e93e726f1daa75f5", + "reference": "7c62f41fb0460c3e5d5c1f70e93e726f1daa75f5", "shasum": "" }, "require": { @@ -290,9 +290,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.12" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.15" }, - "time": "2026-01-13T19:12:08+00:00" + "time": "2026-01-16T19:18:57+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -575,16 +575,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.9", + "version": "1.5.10", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "1905981ee626e6f852448b7aaa978f8666c5bc54" + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/1905981ee626e6f852448b7aaa978f8666c5bc54", - "reference": "1905981ee626e6f852448b7aaa978f8666c5bc54", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63", + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63", "shasum": "" }, "require": { @@ -631,7 +631,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.9" + "source": "https://github.com/composer/ca-bundle/tree/1.5.10" }, "funding": [ { @@ -643,7 +643,7 @@ "type": "github" } ], - "time": "2025-11-06T11:46:17+00:00" + "time": "2025-12-08T15:06:51+00:00" }, { "name": "danharrin/date-format-converter", @@ -827,16 +827,16 @@ }, { "name": "doctrine/dbal", - "version": "3.10.3", + "version": "3.10.4", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "65edaca19a752730f290ec2fb89d593cb40afb43" + "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43", - "reference": "65edaca19a752730f290ec2fb89d593cb40afb43", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868", + "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868", "shasum": "" }, "require": { @@ -860,8 +860,8 @@ "phpunit/phpunit": "9.6.29", "slevomat/coding-standard": "8.24.0", "squizlabs/php_codesniffer": "4.0.0", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0" + "symfony/cache": "^5.4|^6.0|^7.0|^8.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -921,7 +921,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.3" + "source": "https://github.com/doctrine/dbal/tree/3.10.4" }, "funding": [ { @@ -937,7 +937,7 @@ "type": "tidelift" } ], - "time": "2025-10-09T09:05:12+00:00" + "time": "2025-11-29T10:46:08+00:00" }, { "name": "doctrine/deprecations", @@ -989,16 +989,16 @@ }, { "name": "doctrine/event-manager", - "version": "2.0.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + "reference": "c07799fcf5ad362050960a0fd068dded40b1e312" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/c07799fcf5ad362050960a0fd068dded40b1e312", + "reference": "c07799fcf5ad362050960a0fd068dded40b1e312", "shasum": "" }, "require": { @@ -1008,10 +1008,10 @@ "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -1060,7 +1060,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + "source": "https://github.com/doctrine/event-manager/tree/2.1.0" }, "funding": [ { @@ -1076,7 +1076,7 @@ "type": "tidelift" } ], - "time": "2024-05-22T20:47:39+00:00" + "time": "2026-01-17T22:40:21+00:00" }, { "name": "doctrine/inflector", @@ -1467,16 +1467,16 @@ }, { "name": "filament/actions", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "4582f2da9ed0660685b8e0849d32f106bc8a4b2d" + "reference": "f8ea2b015b12c00522f1d6a7bcb9453b5f08beb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/4582f2da9ed0660685b8e0849d32f106bc8a4b2d", - "reference": "4582f2da9ed0660685b8e0849d32f106bc8a4b2d", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/f8ea2b015b12c00522f1d6a7bcb9453b5f08beb1", + "reference": "f8ea2b015b12c00522f1d6a7bcb9453b5f08beb1", "shasum": "" }, "require": { @@ -1516,20 +1516,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-09-28T22:06:00+00:00" + "time": "2026-01-01T16:29:27+00:00" }, { "name": "filament/filament", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "1cc3a0b06cb287048c53d49b3915064a8fc6449f" + "reference": "790e3c163e93f5746beea88b93d38673424984b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/1cc3a0b06cb287048c53d49b3915064a8fc6449f", - "reference": "1cc3a0b06cb287048c53d49b3915064a8fc6449f", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/790e3c163e93f5746beea88b93d38673424984b6", + "reference": "790e3c163e93f5746beea88b93d38673424984b6", "shasum": "" }, "require": { @@ -1581,20 +1581,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-11-11T10:10:18+00:00" + "time": "2026-01-01T16:29:34+00:00" }, { "name": "filament/forms", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "da5401bf3684b6abc6cf1d8e152f01b25d815319" + "reference": "f708ce490cff3770071d18e9ea678eb4b7c65c58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/da5401bf3684b6abc6cf1d8e152f01b25d815319", - "reference": "da5401bf3684b6abc6cf1d8e152f01b25d815319", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/f708ce490cff3770071d18e9ea678eb4b7c65c58", + "reference": "f708ce490cff3770071d18e9ea678eb4b7c65c58", "shasum": "" }, "require": { @@ -1637,20 +1637,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-10-06T21:42:10+00:00" + "time": "2026-01-01T16:29:33+00:00" }, { "name": "filament/infolists", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "5a519cf36a20039ccba8491a52028a8619cb70cb" + "reference": "ac7fc1c8acc651c6c793696f0772747791c91155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/5a519cf36a20039ccba8491a52028a8619cb70cb", - "reference": "5a519cf36a20039ccba8491a52028a8619cb70cb", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/ac7fc1c8acc651c6c793696f0772747791c91155", + "reference": "ac7fc1c8acc651c6c793696f0772747791c91155", "shasum": "" }, "require": { @@ -1688,20 +1688,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-11-11T10:09:16+00:00" + "time": "2026-01-01T16:28:31+00:00" }, { "name": "filament/notifications", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", - "reference": "e94502a23ccdb2a74c7cc408db3291c36371231c" + "reference": "3a6ef54b6a8cefc79858e7033e4d6b65fb2d859b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/notifications/zipball/e94502a23ccdb2a74c7cc408db3291c36371231c", - "reference": "e94502a23ccdb2a74c7cc408db3291c36371231c", + "url": "https://api.github.com/repos/filamentphp/notifications/zipball/3a6ef54b6a8cefc79858e7033e4d6b65fb2d859b", + "reference": "3a6ef54b6a8cefc79858e7033e4d6b65fb2d859b", "shasum": "" }, "require": { @@ -1740,20 +1740,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-11-11T10:09:28+00:00" + "time": "2026-01-01T16:29:16+00:00" }, { "name": "filament/support", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78" + "reference": "c37f4b9045a7c514974e12562b5a41813860b505" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/afafd5e7a2f8cf052f70f989b52d82d0a1df5c78", - "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78", + "url": "https://api.github.com/repos/filamentphp/support/zipball/c37f4b9045a7c514974e12562b5a41813860b505", + "reference": "c37f4b9045a7c514974e12562b5a41813860b505", "shasum": "" }, "require": { @@ -1799,20 +1799,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-08-12T13:15:44+00:00" + "time": "2026-01-09T09:01:14+00:00" }, { "name": "filament/tables", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "2e1e3aeeeccd6b74e5d038325af52635d1108e4c" + "reference": "c88d17248827b3fbca09db53d563498d29c6b180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/2e1e3aeeeccd6b74e5d038325af52635d1108e4c", - "reference": "2e1e3aeeeccd6b74e5d038325af52635d1108e4c", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/c88d17248827b3fbca09db53d563498d29c6b180", + "reference": "c88d17248827b3fbca09db53d563498d29c6b180", "shasum": "" }, "require": { @@ -1851,20 +1851,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-09-17T10:47:13+00:00" + "time": "2026-01-01T16:29:37+00:00" }, { "name": "filament/widgets", - "version": "v3.3.45", + "version": "v3.3.47", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", - "reference": "5b956f884aaef479f6091463cb829e7c9f2afc2c" + "reference": "2bf59fd94007b69c22c161f7a4749ea19560e03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/widgets/zipball/5b956f884aaef479f6091463cb829e7c9f2afc2c", - "reference": "5b956f884aaef479f6091463cb829e7c9f2afc2c", + "url": "https://api.github.com/repos/filamentphp/widgets/zipball/2bf59fd94007b69c22c161f7a4749ea19560e03e", + "reference": "2bf59fd94007b69c22c161f7a4749ea19560e03e", "shasum": "" }, "require": { @@ -1895,20 +1895,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-12T15:11:14+00:00" + "time": "2026-01-01T16:29:32+00:00" }, { "name": "firebase/php-jwt", - "version": "v6.11.1", + "version": "v7.0.2", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", "shasum": "" }, "require": { @@ -1956,37 +1956,37 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + "source": "https://github.com/firebase/php-jwt/tree/v7.0.2" }, - "time": "2025-04-09T20:32:01+00:00" + "time": "2025-12-16T22:17:28+00:00" }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -2017,7 +2017,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -2029,28 +2029,28 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -2079,7 +2079,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -2091,7 +2091,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", @@ -2506,16 +2506,16 @@ }, { "name": "intervention/gif", - "version": "4.2.2", + "version": "4.2.4", "source": { "type": "git", "url": "https://github.com/Intervention/gif.git", - "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a" + "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a", - "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a", + "url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c", + "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c", "shasum": "" }, "require": { @@ -2554,7 +2554,7 @@ ], "support": { "issues": "https://github.com/Intervention/gif/issues", - "source": "https://github.com/Intervention/gif/tree/4.2.2" + "source": "https://github.com/Intervention/gif/tree/4.2.4" }, "funding": [ { @@ -2570,20 +2570,20 @@ "type": "ko_fi" } ], - "time": "2025-03-29T07:46:21+00:00" + "time": "2026-01-04T09:27:23+00:00" }, { "name": "intervention/image", - "version": "3.11.4", + "version": "3.11.6", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "8c49eb21a6d2572532d1bc425964264f3e496846" + "reference": "5f6d27d9fd56312c47f347929e7ac15345c605a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/8c49eb21a6d2572532d1bc425964264f3e496846", - "reference": "8c49eb21a6d2572532d1bc425964264f3e496846", + "url": "https://api.github.com/repos/Intervention/image/zipball/5f6d27d9fd56312c47f347929e7ac15345c605a1", + "reference": "5f6d27d9fd56312c47f347929e7ac15345c605a1", "shasum": "" }, "require": { @@ -2615,11 +2615,11 @@ { "name": "Oliver Vogel", "email": "oliver@intervention.io", - "homepage": "https://intervention.io/" + "homepage": "https://intervention.io" } ], - "description": "PHP image manipulation", - "homepage": "https://image.intervention.io/", + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", "keywords": [ "gd", "image", @@ -2630,7 +2630,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.4" + "source": "https://github.com/Intervention/image/tree/3.11.6" }, "funding": [ { @@ -2646,7 +2646,7 @@ "type": "ko_fi" } ], - "time": "2025-07-30T13:13:19+00:00" + "time": "2025-12-17T13:38:29+00:00" }, { "name": "jean85/pretty-package-versions", @@ -2861,16 +2861,16 @@ }, { "name": "laravel/framework", - "version": "v10.49.1", + "version": "v10.50.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f857267b80789327cd3e6b077bcf6df5846cf71b" + "reference": "fc41c8ceb4d4a55b23d4030ef4ed86383e4b2bc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f857267b80789327cd3e6b077bcf6df5846cf71b", - "reference": "f857267b80789327cd3e6b077bcf6df5846cf71b", + "url": "https://api.github.com/repos/laravel/framework/zipball/fc41c8ceb4d4a55b23d4030ef4ed86383e4b2bc3", + "reference": "fc41c8ceb4d4a55b23d4030ef4ed86383e4b2bc3", "shasum": "" }, "require": { @@ -3065,20 +3065,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-30T14:56:54+00:00" + "time": "2025-11-28T18:20:42+00:00" }, { "name": "laravel/nightwatch", - "version": "v1.21.1", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/laravel/nightwatch.git", - "reference": "c77c70b56802ff1fc743772a497eeea742ee2b38" + "reference": "a6ef3f6bccc81e69e17e4f67992c1a3ab6a85110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/nightwatch/zipball/c77c70b56802ff1fc743772a497eeea742ee2b38", - "reference": "c77c70b56802ff1fc743772a497eeea742ee2b38", + "url": "https://api.github.com/repos/laravel/nightwatch/zipball/a6ef3f6bccc81e69e17e4f67992c1a3ab6a85110", + "reference": "a6ef3f6bccc81e69e17e4f67992c1a3ab6a85110", "shasum": "" }, "require": { @@ -3159,20 +3159,20 @@ "issues": "https://github.com/laravel/nightwatch/issues", "source": "https://github.com/laravel/nightwatch" }, - "time": "2025-12-18T04:19:39+00:00" + "time": "2026-01-15T04:53:20+00:00" }, { "name": "laravel/pennant", - "version": "v1.18.4", + "version": "v1.18.5", "source": { "type": "git", "url": "https://github.com/laravel/pennant.git", - "reference": "b0725624411f9365f915ae4ec69f415d696caf52" + "reference": "c7d824a46b6fa801925dd3b93470382bcc5b2b58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pennant/zipball/b0725624411f9365f915ae4ec69f415d696caf52", - "reference": "b0725624411f9365f915ae4ec69f415d696caf52", + "url": "https://api.github.com/repos/laravel/pennant/zipball/c7d824a46b6fa801925dd3b93470382bcc5b2b58", + "reference": "c7d824a46b6fa801925dd3b93470382bcc5b2b58", "shasum": "" }, "require": { @@ -3235,7 +3235,7 @@ "issues": "https://github.com/laravel/pennant/issues", "source": "https://github.com/laravel/pennant" }, - "time": "2025-11-20T16:27:35+00:00" + "time": "2025-11-27T16:22:11+00:00" }, { "name": "laravel/prompts", @@ -3424,21 +3424,21 @@ }, { "name": "laravel/socialite", - "version": "v5.24.0", + "version": "v5.24.2", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd" + "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/1d19358c28e8951dde6e36603b89d8f09e6cfbfd", - "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd", + "url": "https://api.github.com/repos/laravel/socialite/zipball/5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613", + "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613", "shasum": "" }, "require": { "ext-json": "*", - "firebase/php-jwt": "^6.4", + "firebase/php-jwt": "^6.4|^7.0", "guzzlehttp/guzzle": "^6.0|^7.0", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", @@ -3492,20 +3492,20 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-12-09T15:37:06+00:00" + "time": "2026-01-10T16:07:28+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.2", + "version": "v2.11.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c" + "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c", - "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c", + "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468", + "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468", "shasum": "" }, "require": { @@ -3514,7 +3514,7 @@ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", @@ -3556,22 +3556,22 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.2" + "source": "https://github.com/laravel/tinker/tree/v2.11.0" }, - "time": "2025-11-20T16:29:12+00:00" + "time": "2025-12-19T19:16:45+00:00" }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", "shasum": "" }, "require": { @@ -3608,7 +3608,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -3665,7 +3665,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2025-11-26T21:48:24+00:00" }, { "name": "league/config", @@ -3751,16 +3751,16 @@ }, { "name": "league/csv", - "version": "9.27.1", + "version": "9.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797" + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/26de738b8fccf785397d05ee2fc07b6cd8749797", - "reference": "26de738b8fccf785397d05ee2fc07b6cd8749797", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/6582ace29ae09ba5b07049d40ea13eb19c8b5073", + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073", "shasum": "" }, "require": { @@ -3770,14 +3770,14 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^1.12.27", + "friendsofphp/php-cs-fixer": "^3.92.3", + "phpbench/phpbench": "^1.4.3", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.2", "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6", - "symfony/var-dumper": "^6.4.8 || ^7.3.0" + "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.5.4", + "symfony/var-dumper": "^6.4.8 || ^7.4.0 || ^8.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", @@ -3838,7 +3838,7 @@ "type": "github" } ], - "time": "2025-10-25T08:35:20+00:00" + "time": "2025-12-27T15:18:42+00:00" }, { "name": "league/flysystem", @@ -4161,20 +4161,20 @@ }, { "name": "league/uri", - "version": "7.6.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "f625804987a0a9112d954f9209d91fec52182344" + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", - "reference": "f625804987a0a9112d954f9209d91fec52182344", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.6", + "league/uri-interfaces": "^7.8", "php": "^8.1", "psr/http-factory": "^1" }, @@ -4188,11 +4188,11 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", - "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -4247,7 +4247,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.6.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.0" }, "funding": [ { @@ -4255,20 +4255,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2026-01-14T17:24:56+00:00" }, { "name": "league/uri-interfaces", - "version": "7.6.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", "shasum": "" }, "require": { @@ -4281,7 +4281,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -4331,7 +4331,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" }, "funding": [ { @@ -4339,20 +4339,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "livewire/livewire", - "version": "v3.7.0", + "version": "v3.7.4", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb" + "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", - "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb", + "url": "https://api.github.com/repos/livewire/livewire/zipball/5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", + "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", "shasum": "" }, "require": { @@ -4407,7 +4407,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.0" + "source": "https://github.com/livewire/livewire/tree/v3.7.4" }, "funding": [ { @@ -4415,7 +4415,7 @@ "type": "github" } ], - "time": "2025-11-12T17:58:16+00:00" + "time": "2026-01-13T09:37:21+00:00" }, { "name": "masterminds/html5", @@ -4680,16 +4680,16 @@ }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -4707,7 +4707,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -4767,7 +4767,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -4779,7 +4779,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "mtdowling/jmespath.php", @@ -5021,20 +5021,20 @@ }, { "name": "nette/utils", - "version": "v4.0.9", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "505a30ad386daa5211f08a318e47015b501cad30" + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/505a30ad386daa5211f08a318e47015b501cad30", - "reference": "505a30ad386daa5211f08a318e47015b501cad30", + "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -5057,7 +5057,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -5104,22 +5104,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.9" + "source": "https://github.com/nette/utils/tree/v4.1.1" }, - "time": "2025-10-31T00:45:47+00:00" + "time": "2025-12-22T12:14:32+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -5162,9 +5162,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", @@ -5596,16 +5596,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -5655,7 +5655,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -5667,7 +5667,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", @@ -6242,16 +6242,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.14", + "version": "v0.12.18", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "95c29b3756a23855a30566b745d218bee690bef2" + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", - "reference": "95c29b3756a23855a30566b745d218bee690bef2", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "shasum": "" }, "require": { @@ -6259,8 +6259,8 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -6315,9 +6315,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" }, - "time": "2025-10-27T17:15:31+00:00" + "time": "2025-12-17T14:35:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -6441,20 +6441,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -6513,9 +6513,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "ryangjchandler/blade-capture-directive", @@ -6597,16 +6597,16 @@ }, { "name": "sentry/sentry", - "version": "4.18.1", + "version": "4.19.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "04dcf20b39742b731b676f8b8d4f02d1db488af8" + "reference": "1c21d60bebe67c0122335bd3fe977990435af0a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/04dcf20b39742b731b676f8b8d4f02d1db488af8", - "reference": "04dcf20b39742b731b676f8b8d4f02d1db488af8", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/1c21d60bebe67c0122335bd3fe977990435af0a3", + "reference": "1c21d60bebe67c0122335bd3fe977990435af0a3", "shasum": "" }, "require": { @@ -6669,7 +6669,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.18.1" + "source": "https://github.com/getsentry/sentry-php/tree/4.19.1" }, "funding": [ { @@ -6681,28 +6681,28 @@ "type": "custom" } ], - "time": "2025-11-11T09:34:53+00:00" + "time": "2025-12-02T15:57:41+00:00" }, { "name": "sentry/sentry-laravel", - "version": "4.19.0", + "version": "4.20.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f" + "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f", - "reference": "7fdffd57e8fff0a6f9a18d9a83f32e960af63e3f", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/503853fa7ee74b34b64e76f1373db86cd11afe72", + "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72", "shasum": "" }, "require": { "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", - "sentry/sentry": "^4.18.0", - "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" + "sentry/sentry": "^4.19.0", + "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.11", @@ -6759,7 +6759,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.19.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.20.1" }, "funding": [ { @@ -6771,7 +6771,7 @@ "type": "custom" } ], - "time": "2025-11-11T09:01:14+00:00" + "time": "2026-01-07T08:53:19+00:00" }, { "name": "simonhamp/the-og", @@ -7380,16 +7380,16 @@ }, { "name": "symfony/console", - "version": "v6.4.27", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" + "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", + "url": "https://api.github.com/repos/symfony/console/zipball/f9f8a889f54c264f9abac3fc0f7a371ffca51997", + "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997", "shasum": "" }, "require": { @@ -7454,7 +7454,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.27" + "source": "https://github.com/symfony/console/tree/v6.4.31" }, "funding": [ { @@ -7474,24 +7474,24 @@ "type": "tidelift" } ], - "time": "2025-10-06T10:25:16+00:00" + "time": "2025-12-22T08:30:34+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.6", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "84321188c4754e64273b46b406081ad9b18e8614" + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/84321188c4754e64273b46b406081ad9b18e8614", - "reference": "84321188c4754e64273b46b406081ad9b18e8614", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b", + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -7523,7 +7523,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.6" + "source": "https://github.com/symfony/css-selector/tree/v8.0.0" }, "funding": [ { @@ -7543,7 +7543,7 @@ "type": "tidelift" } ], - "time": "2025-10-29T17:24:25+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/deprecation-contracts", @@ -7693,16 +7693,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { @@ -7719,13 +7719,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7753,7 +7754,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" }, "funding": [ { @@ -7773,7 +7774,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7923,16 +7924,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.27", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b" + "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/a1b6aa435d2fba50793b994a839c32b6064f063b", - "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b", + "url": "https://api.github.com/repos/symfony/finder/zipball/5547f2e1f0ca8e2e7abe490156b62da778cfbe2b", + "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b", "shasum": "" }, "require": { @@ -7967,7 +7968,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.27" + "source": "https://github.com/symfony/finder/tree/v6.4.31" }, "funding": [ { @@ -7987,27 +7988,28 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:32:00+00:00" + "time": "2025-12-11T14:52:17+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "3855e827adb1b675adcb98ad7f92681e293f2d77" + "reference": "5b0bbcc3600030b535dd0b17a0e8c56243f96d7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/3855e827adb1b675adcb98ad7f92681e293f2d77", - "reference": "3855e827adb1b675adcb98ad7f92681e293f2d77", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/5b0bbcc3600030b535dd0b17a0e8c56243f96d7f", + "reference": "5b0bbcc3600030b535dd0b17a0e8c56243f96d7f", "shasum": "" }, "require": { "ext-dom": "*", "league/uri": "^6.5|^7.0", "masterminds/html5": "^2.7.2", - "php": ">=8.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -8040,7 +8042,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v7.3.6" + "source": "https://github.com/symfony/html-sanitizer/tree/v7.4.0" }, "funding": [ { @@ -8060,20 +8062,20 @@ "type": "tidelift" } ], - "time": "2025-10-30T13:22:58+00:00" + "time": "2025-10-30T13:39:42+00:00" }, { "name": "symfony/http-client", - "version": "v7.3.6", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de" + "reference": "d01dfac1e0dc99f18da48b18101c23ce57929616" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", - "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616", + "reference": "d01dfac1e0dc99f18da48b18101c23ce57929616", "shasum": "" }, "require": { @@ -8104,12 +8106,13 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -8140,7 +8143,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.6" + "source": "https://github.com/symfony/http-client/tree/v7.4.3" }, "funding": [ { @@ -8160,7 +8163,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T17:41:46+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/http-client-contracts", @@ -8242,16 +8245,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.29", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "b03d11e015552a315714c127d8d1e0f9e970ec88" + "reference": "a35ee6f47e4775179704d7877a8b0da3cb09241a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b03d11e015552a315714c127d8d1e0f9e970ec88", - "reference": "b03d11e015552a315714c127d8d1e0f9e970ec88", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a35ee6f47e4775179704d7877a8b0da3cb09241a", + "reference": "a35ee6f47e4775179704d7877a8b0da3cb09241a", "shasum": "" }, "require": { @@ -8299,7 +8302,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.29" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.31" }, "funding": [ { @@ -8319,20 +8322,20 @@ "type": "tidelift" } ], - "time": "2025-11-08T16:40:12+00:00" + "time": "2025-12-17T10:10:57+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.29", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "18818b48f54c1d2bd92b41d82d8345af50b15658" + "reference": "16b0d46d8e11f480345c15b229cfc827a8a0f731" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/18818b48f54c1d2bd92b41d82d8345af50b15658", - "reference": "18818b48f54c1d2bd92b41d82d8345af50b15658", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/16b0d46d8e11f480345c15b229cfc827a8a0f731", + "reference": "16b0d46d8e11f480345c15b229cfc827a8a0f731", "shasum": "" }, "require": { @@ -8417,7 +8420,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.29" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.31" }, "funding": [ { @@ -8437,20 +8440,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T11:22:59+00:00" + "time": "2025-12-31T08:27:27+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.27", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "2f096718ed718996551f66e3a24e12b2ed027f95" + "reference": "8835f93333474780fda1b987cae37e33c3e026ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/2f096718ed718996551f66e3a24e12b2ed027f95", - "reference": "2f096718ed718996551f66e3a24e12b2ed027f95", + "url": "https://api.github.com/repos/symfony/mailer/zipball/8835f93333474780fda1b987cae37e33c3e026ca", + "reference": "8835f93333474780fda1b987cae37e33c3e026ca", "shasum": "" }, "require": { @@ -8501,7 +8504,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.27" + "source": "https://github.com/symfony/mailer/tree/v6.4.31" }, "funding": [ { @@ -8521,7 +8524,7 @@ "type": "tidelift" } ], - "time": "2025-10-24T13:29:09+00:00" + "time": "2025-12-12T07:33:25+00:00" }, { "name": "symfony/mailgun-mailer", @@ -8594,16 +8597,16 @@ }, { "name": "symfony/mime", - "version": "v6.4.26", + "version": "v6.4.30", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235" + "reference": "69aeef5d2692bb7c18ce133b09f67b27260b7acf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/61ab9681cdfe315071eb4fa79b6ad6ab030a9235", - "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235", + "url": "https://api.github.com/repos/symfony/mime/zipball/69aeef5d2692bb7c18ce133b09f67b27260b7acf", + "reference": "69aeef5d2692bb7c18ce133b09f67b27260b7acf", "shasum": "" }, "require": { @@ -8659,7 +8662,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.26" + "source": "https://github.com/symfony/mime/tree/v6.4.30" }, "funding": [ { @@ -8679,24 +8682,24 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:22:30+00:00" + "time": "2025-11-16T09:57:53+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -8730,7 +8733,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -8750,7 +8753,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -9591,16 +9594,16 @@ }, { "name": "symfony/process", - "version": "v6.4.26", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8" + "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/48bad913268c8cafabbf7034b39c8bb24fbc5ab8", - "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8", + "url": "https://api.github.com/repos/symfony/process/zipball/8541b7308fca001320e90bca8a73a28aa5604a6e", + "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e", "shasum": "" }, "require": { @@ -9632,7 +9635,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.26" + "source": "https://github.com/symfony/process/tree/v6.4.31" }, "funding": [ { @@ -9652,26 +9655,26 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2025-12-15T19:26:35+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + "reference": "0101ff8bd0506703b045b1670960302d302a726c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/0101ff8bd0506703b045b1670960302d302a726c", + "reference": "0101ff8bd0506703b045b1670960302d302a726c", "shasum": "" }, "require": { "php": ">=8.2", "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^6.4|^7.0" + "symfony/http-foundation": "^6.4|^7.0|^8.0" }, "conflict": { "php-http/discovery": "<1.15", @@ -9681,11 +9684,12 @@ "nyholm/psr7": "^1.1", "php-http/discovery": "^1.15", "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" }, "type": "symfony-bridge", "autoload": { @@ -9719,7 +9723,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.3.0" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.0" }, "funding": [ { @@ -9730,25 +9734,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-26T08:57:56+00:00" + "time": "2025-11-13T08:38:49+00:00" }, { "name": "symfony/routing", - "version": "v6.4.28", + "version": "v6.4.30", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "ae064a6d9cf39507f9797658465a2ca702965fa8" + "reference": "ea50a13c2711eebcbb66b38ef6382e62e3262859" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/ae064a6d9cf39507f9797658465a2ca702965fa8", - "reference": "ae064a6d9cf39507f9797658465a2ca702965fa8", + "url": "https://api.github.com/repos/symfony/routing/zipball/ea50a13c2711eebcbb66b38ef6382e62e3262859", + "reference": "ea50a13c2711eebcbb66b38ef6382e62e3262859", "shasum": "" }, "require": { @@ -9802,7 +9810,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.28" + "source": "https://github.com/symfony/routing/tree/v6.4.30" }, "funding": [ { @@ -9822,7 +9830,7 @@ "type": "tidelift" } ], - "time": "2025-10-31T16:43:05+00:00" + "time": "2025-11-22T09:51:35+00:00" }, { "name": "symfony/service-contracts", @@ -9913,22 +9921,23 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -9936,11 +9945,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -9979,7 +9988,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.0" }, "funding": [ { @@ -9999,20 +10008,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation", - "version": "v6.4.26", + "version": "v6.4.31", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4" + "reference": "81579408ecf7dc5aa2d8462a6d5c3a430a80e6f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/c8559fe25c7ee7aa9d28f228903a46db008156a4", - "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4", + "url": "https://api.github.com/repos/symfony/translation/zipball/81579408ecf7dc5aa2d8462a6d5c3a430a80e6f2", + "reference": "81579408ecf7dc5aa2d8462a6d5c3a430a80e6f2", "shasum": "" }, "require": { @@ -10078,7 +10087,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.26" + "source": "https://github.com/symfony/translation/tree/v6.4.31" }, "funding": [ { @@ -10098,7 +10107,7 @@ "type": "tidelift" } ], - "time": "2025-09-05T18:17:25+00:00" + "time": "2025-12-18T11:37:55+00:00" }, { "name": "symfony/translation-contracts", @@ -10350,28 +10359,28 @@ }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -10402,7 +10411,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -10422,27 +10431,27 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -10475,9 +10484,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "torchlight/torchlight-commonmark", @@ -10601,26 +10610,26 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -10669,7 +10678,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -10681,7 +10690,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -10946,19 +10955,20 @@ }, { "name": "illuminate/json-schema", - "version": "v12.40.1", + "version": "v12.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/json-schema.git", - "reference": "c2b383a6dd66f41208f1443801fe01934c63d030" + "reference": "d161f398dab36f08cf131997362bc2e3ecb0309a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/json-schema/zipball/c2b383a6dd66f41208f1443801fe01934c63d030", - "reference": "c2b383a6dd66f41208f1443801fe01934c63d030", + "url": "https://api.github.com/repos/illuminate/json-schema/zipball/d161f398dab36f08cf131997362bc2e3ecb0309a", + "reference": "d161f398dab36f08cf131997362bc2e3ecb0309a", "shasum": "" }, "require": { + "illuminate/contracts": "^10.50.0|^11.47.0|^12.40.2", "php": "^8.1" }, "type": "library", @@ -10988,38 +10998,38 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-03T22:27:03+00:00" + "time": "2025-11-28T18:45:48+00:00" }, { "name": "laravel/boost", - "version": "v1.8.2", + "version": "v1.8.10", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "cf57ba510df44e0d4ed2c1c91360477e92d7d644" + "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/cf57ba510df44e0d4ed2c1c91360477e92d7d644", - "reference": "cf57ba510df44e0d4ed2c1c91360477e92d7d644", + "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32", + "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "laravel/mcp": "^0.3.4", + "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", + "laravel/mcp": "^0.5.1", "laravel/prompts": "0.1.25|^0.3.6", "laravel/roster": "^0.2.9", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20", + "laravel/pint": "^1.20.0", "mockery/mockery": "^1.6.12", "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", - "pestphp/pest": "^2.36.0|^3.8.4", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.1" }, @@ -11054,38 +11064,38 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-11-20T18:13:17+00:00" + "time": "2026-01-14T14:51:16+00:00" }, { "name": "laravel/mcp", - "version": "v0.3.4", + "version": "v0.5.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77" + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/0b86fb613a0df971cec89271c674a677c2cb4f77", - "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77", + "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c", + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/json-schema": "^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/container": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/http": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/json-schema": "^12.41.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20.0", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6.0", + "laravel/pint": "^1.20", + "orchestra/testbench": "^8.36|^9.15|^10.8", "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.2.4" @@ -11127,20 +11137,20 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-11-18T14:41:05+00:00" + "time": "2025-12-19T19:32:34+00:00" }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", "shasum": "" }, "require": { @@ -11151,13 +11161,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.92.4", + "illuminate/view": "^12.44.0", + "larastan/larastan": "^3.8.1", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -11183,6 +11193,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -11193,7 +11204,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2026-01-05T16:49:17+00:00" }, { "name": "laravel/roster", @@ -11258,16 +11269,16 @@ }, { "name": "laravel/sail", - "version": "v1.48.1", + "version": "v1.52.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "ef122b223f5fca5e5d88bda5127c846710886329" + "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/ef122b223f5fca5e5d88bda5127c846710886329", - "reference": "ef122b223f5fca5e5d88bda5127c846710886329", + "url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3", + "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3", "shasum": "" }, "require": { @@ -11317,7 +11328,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-11-17T22:05:34+00:00" + "time": "2026-01-01T02:46:03+00:00" }, { "name": "mockery/mockery", @@ -11999,16 +12010,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.60", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "f2e26f52f80ef77832e359205f216eeac00e320c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c", + "reference": "f2e26f52f80ef77832e359205f216eeac00e320c", "shasum": "" }, "require": { @@ -12080,7 +12091,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60" }, "funding": [ { @@ -12104,7 +12115,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2025-12-06T07:50:42+00:00" }, { "name": "sebastian/cli-parser",