From bd7698674eaea87032a2ba51c77903d97c24f25b Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 30 Mar 2026 11:34:33 +0100 Subject: [PATCH 1/7] Add admin UX improvements: impersonate users, mobile viewport, ticket UI - Install filament-impersonate for user impersonation from admin - Add impersonate button to Users table, Edit User, and View Support Ticket pages - Add impersonate banner to customer dashboard layout - Disable pinch-to-zoom on admin panel for mobile usability - Show user name with email in support ticket User column - Improve code block contrast in ticket replies with explicit styles - Remove keyboard shortcut hint from ticket reply form Co-Authored-By: Claude Opus 4.6 --- .../Resources/SupportTicketResource.php | 8 ++-- .../Pages/ViewSupportTicket.php | 3 ++ app/Filament/Resources/UserResource.php | 2 + .../Resources/UserResource/Pages/EditUser.php | 3 ++ app/Providers/Filament/AdminPanelProvider.php | 5 ++ composer.json | 1 + composer.lock | 48 ++++++++++++++++++- package-lock.json | 2 +- .../components/layouts/dashboard.blade.php | 2 + .../widgets/ticket-replies.blade.php | 27 ++++++++++- 10 files changed, 94 insertions(+), 7 deletions(-) diff --git a/app/Filament/Resources/SupportTicketResource.php b/app/Filament/Resources/SupportTicketResource.php index 2b1840cdd..2a9e05397 100644 --- a/app/Filament/Resources/SupportTicketResource.php +++ b/app/Filament/Resources/SupportTicketResource.php @@ -51,8 +51,9 @@ public static function infolist(Schema $schema): Schema Infolists\Components\TextEntry::make('issue_type') ->label('Issue Type') ->placeholder('N/A'), - Infolists\Components\TextEntry::make('user.email') + Infolists\Components\TextEntry::make('user.name') ->label('User') + ->formatStateUsing(fn (SupportTicket $record): string => ($record->user->name ?? '').' ('.$record->user->email.')') ->url(fn (SupportTicket $record): string => UserResource::getUrl('edit', ['record' => $record->user_id])), Infolists\Components\TextEntry::make('created_at') ->label('Created') @@ -96,9 +97,10 @@ public static function table(Table $table): Table ->sortable() ->limit(50), - Tables\Columns\TextColumn::make('user.email') + Tables\Columns\TextColumn::make('user.name') ->label('User') - ->searchable() + ->formatStateUsing(fn (SupportTicket $record): string => ($record->user->name ?? '').' ('.$record->user->email.')') + ->searchable(query: fn ($query, string $search) => $query->whereHas('user', fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('email', 'like', "%{$search}%"))) ->sortable(), Tables\Columns\TextColumn::make('product') diff --git a/app/Filament/Resources/SupportTicketResource/Pages/ViewSupportTicket.php b/app/Filament/Resources/SupportTicketResource/Pages/ViewSupportTicket.php index e4edfd124..a28f490af 100644 --- a/app/Filament/Resources/SupportTicketResource/Pages/ViewSupportTicket.php +++ b/app/Filament/Resources/SupportTicketResource/Pages/ViewSupportTicket.php @@ -8,6 +8,7 @@ use Filament\Actions; use Filament\Forms\Components\Select; use Filament\Resources\Pages\ViewRecord; +use STS\FilamentImpersonate\Actions\Impersonate; class ViewSupportTicket extends ViewRecord { @@ -16,6 +17,8 @@ class ViewSupportTicket extends ViewRecord protected function getHeaderActions(): array { return [ + Impersonate::make()->impersonateRecord(fn () => $this->getRecord()->user), + Actions\Action::make('updateStatus') ->label('Update Status') ->icon('heroicon-o-arrow-path') diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index d95da4841..981bdfad2 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -12,6 +12,7 @@ use Filament\Schemas\Schema; use Filament\Tables; use Filament\Tables\Table; +use STS\FilamentImpersonate\Actions\Impersonate; class UserResource extends Resource { @@ -89,6 +90,7 @@ public static function table(Table $table): Table // ]) ->actions([ + Impersonate::make(), Actions\ActionGroup::make([ Actions\EditAction::make(), Actions\Action::make('view_on_stripe') diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 5774ad7c5..5843618a5 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -13,6 +13,7 @@ use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Facades\Password; +use STS\FilamentImpersonate\Actions\Impersonate; class EditUser extends EditRecord { @@ -21,6 +22,8 @@ class EditUser extends EditRecord protected function getHeaderActions(): array { return [ + Impersonate::make()->record($this->getRecord()), + Actions\ActionGroup::make([ Actions\Action::make('createStripeCustomer') ->label('Create Stripe Customer') diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 75aa22f08..0d2d26a4e 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -10,6 +10,7 @@ use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; +use Filament\View\PanelsRenderHook; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -33,6 +34,10 @@ public function panel(Panel $panel): Panel ]) ->brandName('NativePHP Admin') ->favicon(asset('favicon.ico')) + ->renderHook( + PanelsRenderHook::HEAD_END, + fn (): string => '', + ) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->pages([ diff --git a/composer.json b/composer.json index aa8d99a9a..073432bb1 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "simonhamp/the-og": "^0.7.0", "spatie/laravel-menu": "^4.1", "spatie/yaml-front-matter": "^2.0", + "stechstudio/filament-impersonate": "^5.1", "symfony/http-client": "^7.2", "symfony/mailgun-mailer": "^7.1", "torchlight/torchlight-commonmark": "^0.6.0" diff --git a/composer.lock b/composer.lock index 02daeaaf2..ad890c831 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0cc77dd249810289ccbf001bbaec7aa5", + "content-hash": "8997aa561e2f9fd8f796a63500a97e5b", "packages": [ { "name": "artesaos/seotools", @@ -7958,6 +7958,52 @@ ], "time": "2025-11-24T16:17:28+00:00" }, + { + "name": "stechstudio/filament-impersonate", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/stechstudio/filament-impersonate.git", + "reference": "c5d5b8b5150fad02db18c4479eafb9dc2f537ffc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stechstudio/filament-impersonate/zipball/c5d5b8b5150fad02db18c4479eafb9dc2f537ffc", + "reference": "c5d5b8b5150fad02db18c4479eafb9dc2f537ffc", + "shasum": "" + }, + "require": { + "filament/filament": "^4.0|^5.0" + }, + "require-dev": { + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "STS\\FilamentImpersonate\\FilamentImpersonateServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "STS\\FilamentImpersonate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A Filament package to impersonate your users.", + "support": { + "issues": "https://github.com/stechstudio/filament-impersonate/issues", + "source": "https://github.com/stechstudio/filament-impersonate/tree/v5.1.0" + }, + "time": "2026-02-22T15:57:37+00:00" + }, { "name": "stripe/stripe-php", "version": "v16.6.0", diff --git a/package-lock.json b/package-lock.json index 1ca1fd9b0..3a761fe2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "cyan-grizzly", + "name": "golden-poodle", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/resources/views/components/layouts/dashboard.blade.php b/resources/views/components/layouts/dashboard.blade.php index b54860963..335bdafe8 100644 --- a/resources/views/components/layouts/dashboard.blade.php +++ b/resources/views/components/layouts/dashboard.blade.php @@ -242,6 +242,8 @@ class="min-h-screen bg-white font-poppins antialiased dark:bg-zinc-900 dark:text {{ $slot }} + + @livewireScriptConfig @fluxScripts @vite('resources/js/app.js') diff --git a/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php b/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php index 61bf9a6a9..5122de92a 100644 --- a/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php +++ b/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php @@ -1,4 +1,28 @@ + {{-- Reply form at top --}}
@@ -22,7 +46,6 @@ Internal note (not visible to user)
- ⌘/Ctrl + Enter to send Send Reply Sending... @@ -66,7 +89,7 @@ {{ $reply->created_at->diffForHumans() }}
-
{!! App\Support\CommonMark\CommonMark::convertToHtml($reply->message) !!}
+
{!! App\Support\CommonMark\CommonMark::convertToHtml($reply->message) !!}
@empty

No replies yet.

From 181257507d974b520e9538054e6bef7bc97c8570 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 30 Mar 2026 12:14:27 +0100 Subject: [PATCH 2/7] Add Ultra benefits page, fix pricing to use config values, and UI improvements - Add /dashboard/ultra page showing subscription status, benefits list, and included plugins - Replace all hardcoded pricing/seat values with config() calls across views - Add sticky mobile header in dashboard for persistent nav access - Change Ultra upsell banner icon from sparkles to bolt - Update plugin pricing: Ultra subscribers see real prices with "Included with Ultra" labels - Rename "Pro/Max Subscriber" tier to "Subscriber" Co-Authored-By: Claude Opus 4.6 --- app/Enums/PriceTier.php | 4 +- app/Http/Controllers/UltraController.php | 37 ++++ app/Models/Plugin.php | 10 -- resources/views/cart/show.blade.php | 2 +- resources/views/cart/success.blade.php | 2 +- .../components/layouts/dashboard.blade.php | 8 +- .../views/components/plugin-card.blade.php | 2 +- .../pricing-plan-features.blade.php | 4 +- resources/views/customer/team/index.blade.php | 4 +- .../views/customer/ultra/index.blade.php | 170 ++++++++++++++++++ resources/views/license/renewal.blade.php | 4 +- .../livewire/customer/dashboard.blade.php | 6 +- .../views/livewire/mobile-pricing.blade.php | 10 +- resources/views/plugin-show.blade.php | 38 +++- resources/views/pricing.blade.php | 4 +- resources/views/sponsoring.blade.php | 2 +- routes/web.php | 4 + tests/Feature/UltraBenefitsPageTest.php | 96 ++++++++++ tests/Feature/UltraPluginAccessTest.php | 65 +------ 19 files changed, 373 insertions(+), 99 deletions(-) create mode 100644 app/Http/Controllers/UltraController.php create mode 100644 resources/views/customer/ultra/index.blade.php create mode 100644 tests/Feature/UltraBenefitsPageTest.php diff --git a/app/Enums/PriceTier.php b/app/Enums/PriceTier.php index 072f68f01..b982fb429 100644 --- a/app/Enums/PriceTier.php +++ b/app/Enums/PriceTier.php @@ -12,7 +12,7 @@ public function label(): string { return match ($this) { self::Regular => 'Regular', - self::Subscriber => 'Pro/Max Subscriber', + self::Subscriber => 'Subscriber', self::Eap => 'Early Access', }; } @@ -21,7 +21,7 @@ public function description(): string { return match ($this) { self::Regular => 'Standard pricing for all customers', - self::Subscriber => 'Discounted pricing for Pro and Max license holders', + self::Subscriber => 'Discounted pricing for subscribers', self::Eap => 'Special pricing for Early Access Program customers', }; } diff --git a/app/Http/Controllers/UltraController.php b/app/Http/Controllers/UltraController.php new file mode 100644 index 000000000..fc0994920 --- /dev/null +++ b/app/Http/Controllers/UltraController.php @@ -0,0 +1,37 @@ +hasActiveUltraSubscription()) { + return to_route('pricing'); + } + + $subscription = $user->subscription(); + + $plugins = Plugin::query() + ->where('is_official', true) + ->where('is_active', true) + ->where('status', PluginStatus::Approved) + ->where('type', PluginType::Paid) + ->orderBy('name') + ->get(); + + return view('customer.ultra.index', [ + 'subscription' => $subscription, + 'plugins' => $plugins, + ]); + } +} diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index ee1b5822e..86d26153e 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -185,16 +185,6 @@ public function getBestPriceForUser(?User $user): ?PluginPrice ->orderBy('amount', 'asc') ->first(); - // Ultra subscribers get official plugins for free - if ($bestPrice && $user && $this->isOfficial() && $user->hasUltraAccess()) { - $freePrice = $bestPrice->replicate(); - $freePrice->amount = 0; - $freePrice->id = $bestPrice->id; - $freePrice->exists = true; - - return $freePrice; - } - return $bestPrice; } diff --git a/resources/views/cart/show.blade.php b/resources/views/cart/show.blade.php index d014e8eb1..b76e116b6 100644 --- a/resources/views/cart/show.blade.php +++ b/resources/views/cart/show.blade.php @@ -236,7 +236,7 @@ class="flex gap-4 p-6"

{{ $item->getFormattedPrice() }}

- @if ($item->price_at_addition === 0 && $item->plugin->isOfficial()) + @if ($item->plugin->isOfficial() && auth()->user()?->hasUltraAccess()) Included with Ultra @endif diff --git a/resources/views/cart/success.blade.php b/resources/views/cart/success.blade.php index 972975deb..9dc35fdfd 100644 --- a/resources/views/cart/success.blade.php +++ b/resources/views/cart/success.blade.php @@ -27,7 +27,7 @@
{{ $item->plugin->name }} - @if ($item->price_at_addition === 0 && $item->plugin->isOfficial()) + @if ($item->plugin->isOfficial() && auth()->user()?->hasUltraAccess())

Included with Ultra

@endif
diff --git a/resources/views/components/layouts/dashboard.blade.php b/resources/views/components/layouts/dashboard.blade.php index 335bdafe8..ee150069c 100644 --- a/resources/views/components/layouts/dashboard.blade.php +++ b/resources/views/components/layouts/dashboard.blade.php @@ -111,6 +111,12 @@ class="min-h-screen bg-white font-poppins antialiased dark:bg-zinc-900 dark:text Purchase History + @if(auth()->user()->hasActiveUltraSubscription()) + + Ultra + + @endif + @if(auth()->user()->hasUltraAccess()) Support Tickets @@ -214,7 +220,7 @@ class="min-h-screen bg-white font-poppins antialiased dark:bg-zinc-900 dark:text {{-- Mobile header with sidebar toggle --}} - + diff --git a/resources/views/components/plugin-card.blade.php b/resources/views/components/plugin-card.blade.php index 498abd3f5..27911fb46 100644 --- a/resources/views/components/plugin-card.blade.php +++ b/resources/views/components/plugin-card.blade.php @@ -22,7 +22,7 @@ class="size-12 shrink-0 rounded-xl object-cover" @endif @if ($plugin->isPaid() && $plugin->isOfficial() && auth()->user()?->hasUltraAccess()) - Free with Ultra + Included with Ultra @elseif ($plugin->isPaid()) diff --git a/resources/views/components/pricing-plan-features.blade.php b/resources/views/components/pricing-plan-features.blade.php index 24a0503ab..36d033437 100644 --- a/resources/views/components/pricing-plan-features.blade.php +++ b/resources/views/components/pricing-plan-features.blade.php @@ -56,9 +56,9 @@ class="size-5 shrink-0" />
Teams support — - 5 seats + {{ config('subscriptions.plans.max.included_seats') }} seats included - (extra: $5/seat/mo or $4/seat/yr) + (extra: ${{ config('subscriptions.plans.max.extra_seat_price_monthly') }}/seat/mo or ${{ config('subscriptions.plans.max.extra_seat_price_yearly') }}/seat/yr)
@endif diff --git a/resources/views/customer/team/index.blade.php b/resources/views/customer/team/index.blade.php index 8bb25a277..2a94c4d25 100644 --- a/resources/views/customer/team/index.blade.php +++ b/resources/views/customer/team/index.blade.php @@ -65,7 +65,7 @@ {{-- User has Ultra but no team --}} Create a Team - As an Ultra subscriber, you can create a team and invite up to 4 members who will share your benefits (5 seats total, including you). + As an Ultra subscriber, you can create a team and invite up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members who will share your benefits ({{ config('subscriptions.plans.max.included_seats') }} seats total, including you). @csrf @@ -84,7 +84,7 @@ {{-- User doesn't have Ultra --}} Teams - Teams are available to Ultra subscribers. Upgrade to Ultra to create a team and share benefits with up to 4 members (5 seats total). + Teams are available to Ultra subscribers. Upgrade to Ultra to create a team and share benefits with up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members ({{ config('subscriptions.plans.max.included_seats') }} seats total). View Plans diff --git a/resources/views/customer/ultra/index.blade.php b/resources/views/customer/ultra/index.blade.php new file mode 100644 index 000000000..06edbe1e3 --- /dev/null +++ b/resources/views/customer/ultra/index.blade.php @@ -0,0 +1,170 @@ + +
+
+
+ +
+
+ Ultra + Your premium subscription benefits +
+
+ +
+ {{-- Subscription Status --}} + +
+
+ Subscription + + @if($subscription->stripe_price === config('subscriptions.plans.max.stripe_price_id_monthly')) + ${{ config('subscriptions.plans.max.price_monthly') }}/month + @elseif($subscription->stripe_price === config('subscriptions.plans.max.stripe_price_id_eap')) + ${{ config('subscriptions.plans.max.eap_price_yearly') }}/year (Early Access) + @else + ${{ config('subscriptions.plans.max.price_yearly') }}/year + @endif + +
+ + Manage + +
+
+ + {{-- Benefits --}} + + Your Benefits + Everything included with your Ultra subscription. + +
+
+
+ +
+
+
All first-party plugins
+ Every NativePHP-published plugin is included at no extra cost. New plugins are added automatically. +
+
+ +
+
+ +
+
+
Claude Code Plugin Dev Kit
+ Tools and resources to build NativePHP plugins using Claude Code. + Set up access via Integrations +
+
+ +
+
+ +
+
+
Teams
+ + Invite up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members to share your Ultra benefits ({{ config('subscriptions.plans.max.included_seats') }} seats included). + Extra seats available at ${{ config('subscriptions.plans.max.extra_seat_price_monthly') }}/mo or ${{ config('subscriptions.plans.max.extra_seat_price_yearly') }}/mo on annual plans. + + Manage team +
+
+ +
+
+ +
+
+
Premium support
+ Private support channels with expedited turnaround on your issues. + Support tickets +
+
+ +
+
+ +
+
+
Up to 90% Marketplace revenue
+ Keep up to 90% of earnings on paid plugins you publish to the Marketplace. +
+
+ +
+
+ +
+
+
Exclusive discounts
+ Discounted pricing on NativePHP courses, apps, and future products. +
+
+ +
+
+ +
+
+
Direct repo access on GitHub
+ Access the NativePHP source code repositories directly. +
+
+ +
+
+ +
+
+
Shape the roadmap
+ Help decide feature priority and influence what gets built next. +
+
+
+
+ + {{-- Included Plugins --}} + @if($plugins->isNotEmpty()) +
+ Included Plugins + + + @foreach($plugins as $plugin) + + +
+
+ @if($plugin->hasLogo()) + {{ $plugin->name }} + @elseif($plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif +
+
+ + {{ $plugin->name }} + + @if($plugin->description) + {{ $plugin->description }} + @endif +
+
+
+
+ @endforeach +
+
+
+ @endif +
+
+
diff --git a/resources/views/license/renewal.blade.php b/resources/views/license/renewal.blade.php index 338e3f984..f64eab735 100644 --- a/resources/views/license/renewal.blade.php +++ b/resources/views/license/renewal.blade.php @@ -20,7 +20,7 @@ Recommended Yearly
- $250 + ${{ config('subscriptions.plans.max.eap_price_yearly') }} /year
Early Access Price @@ -41,7 +41,7 @@
Monthly
- $35 + ${{ config('subscriptions.plans.max.price_monthly') }} /month
Billed monthly diff --git a/resources/views/livewire/customer/dashboard.blade.php b/resources/views/livewire/customer/dashboard.blade.php index 72f3c5aa7..18e4620f9 100644 --- a/resources/views/livewire/customer/dashboard.blade.php +++ b/resources/views/livewire/customer/dashboard.blade.php @@ -28,9 +28,7 @@
- - - +

@@ -126,7 +124,7 @@ color="gray" :href="route('customer.team.index')" link-text="Create a team" - description="Invite up to 4 members to share Ultra benefits (5 seats total)" + description="Invite up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members to share Ultra benefits ({{ config('subscriptions.plans.max.included_seats') }} seats total)" /> @endif @endif diff --git a/resources/views/livewire/mobile-pricing.blade.php b/resources/views/livewire/mobile-pricing.blade.php index 1370e4e8a..6bc1650ac 100644 --- a/resources/views/livewire/mobile-pricing.blade.php +++ b/resources/views/livewire/mobile-pricing.blade.php @@ -134,7 +134,7 @@ class="text-3xl font-semibold" @if($isEapCustomer)
- $ + $
@@ -154,11 +154,11 @@ class="flex items-center gap-2 pt-1"
- $ + $
@@ -282,7 +282,7 @@ class="grid size-7 shrink-0 place-items-center rounded-xl bg-[#D4FD7D] dark:bg-[ >
-
5 seats included
+
{{ config('subscriptions.plans.max.included_seats') }} seats included
diff --git a/resources/views/plugin-show.blade.php b/resources/views/plugin-show.blade.php index 9b9e6ec95..203b86fa8 100644 --- a/resources/views/plugin-show.blade.php +++ b/resources/views/plugin-show.blade.php @@ -151,15 +151,7 @@ class="w-full shrink-0 lg:sticky lg:top-24 lg:w-80" @if ($plugin->isPaid() && $bestPrice && $plugin->is_active)
- @if ($bestPrice->amount === 0 && $plugin->isOfficial()) -

Included with Ultra

-

Free

- @if ($regularPrice) -

- ${{ number_format($regularPrice->amount / 100) }} -

- @endif - @elseif ($hasDiscount && $regularPrice) + @if ($hasDiscount && $regularPrice)

Price

${{ number_format($regularPrice->amount / 100) }} @@ -358,6 +350,34 @@ class="size-10 shrink-0 rounded-lg object-cover"

@endif + {{-- Included with Ultra --}} + @if ($plugin->isPaid() && $plugin->isOfficial()) +
+
+
+ +
+
+

Included with Ultra

+

+ You don't need to purchase any first-party NativePHP plugins with Ultra — they're all included for you and your team from just ${{ config('subscriptions.plans.max.price_monthly') }}/month. +

+ @auth +

+ You can still purchase this plugin to keep access even if you cancel your subscription. +

+ @endauth + + Learn more + +
+
+
+ @endif +
diff --git a/resources/views/pricing.blade.php b/resources/views/pricing.blade.php index b0cb706ea..51bb572b9 100644 --- a/resources/views/pricing.blade.php +++ b/resources/views/pricing.blade.php @@ -198,9 +198,9 @@ class="mx-auto flex w-full max-w-2xl flex-col items-center gap-4 pt-10"

- Ultra includes 10 team seats. If you need more, extra + Ultra includes {{ config('subscriptions.plans.max.included_seats') }} team seats. If you need more, extra seats can be purchased from your team settings page - at $5/mo per seat on monthly plans or $4/mo per seat + at ${{ config('subscriptions.plans.max.extra_seat_price_monthly') }}/mo per seat on monthly plans or ${{ config('subscriptions.plans.max.extra_seat_price_yearly') }}/mo per seat on annual plans. Extra seats are billed pro-rata to match your subscription cycle.

diff --git a/resources/views/sponsoring.blade.php b/resources/views/sponsoring.blade.php index a014c32ab..01e682683 100644 --- a/resources/views/sponsoring.blade.php +++ b/resources/views/sponsoring.blade.php @@ -122,7 +122,7 @@ class="prose mt-2 max-w-none text-gray-600 will-change-transform dark:text-gray-

    -
  • Teams — up to 5 seats (you + 4 collaborators) to share your plugin access
  • +
  • Teams — up to {{ config('subscriptions.plans.max.included_seats') }} seats (you + {{ config('subscriptions.plans.max.included_seats') - 1 }} collaborators) to share your plugin access
  • Free official plugins — every NativePHP-published plugin, included with your subscription
  • Plugin Dev Kit — tools and resources to build and publish your own plugins
  • 90% Marketplace revenue — keep up to 90% of earnings on paid plugins you publish
  • diff --git a/routes/web.php b/routes/web.php index 856e37c90..38bc0c866 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,6 +22,7 @@ use App\Http\Controllers\ShowDocumentationController; use App\Http\Controllers\TeamController; use App\Http\Controllers\TeamUserController; +use App\Http\Controllers\UltraController; use App\Livewire\ClaimDonationLicense; use App\Livewire\Customer\Dashboard; use App\Livewire\Customer\Developer\Onboarding; @@ -384,6 +385,9 @@ return $user->redirectToBillingPortal(route('dashboard')); })->name('billing-portal'); + // Ultra benefits page + Route::get('ultra', [UltraController::class, 'index'])->name('ultra.index'); + // Team management routes Route::get('team', [TeamController::class, 'index'])->name('team.index'); Route::post('team', [TeamController::class, 'store'])->name('team.store'); diff --git a/tests/Feature/UltraBenefitsPageTest.php b/tests/Feature/UltraBenefitsPageTest.php new file mode 100644 index 000000000..950b1792b --- /dev/null +++ b/tests/Feature/UltraBenefitsPageTest.php @@ -0,0 +1,96 @@ +create([ + 'stripe_id' => 'cus_'.uniqid(), + ]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + + return $user; + } + + public function test_ultra_subscriber_can_access_benefits_page(): void + { + $user = $this->createUltraSubscriber(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $response->assertStatus(200); + } + + public function test_non_ultra_user_is_redirected_to_pricing(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $response->assertRedirect(route('pricing')); + } + + public function test_guest_is_redirected_to_login(): void + { + $response = $this->get(route('customer.ultra.index')); + + $response->assertRedirect(route('customer.login')); + } + + public function test_benefits_page_displays_expected_content(): void + { + $user = $this->createUltraSubscriber(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $response->assertSee('Ultra'); + $response->assertSee('Your premium subscription benefits'); + $response->assertSee('All first-party plugins'); + $response->assertSee('Claude Code Plugin Dev Kit'); + $response->assertSee('Teams'); + $response->assertSee('Premium support'); + $response->assertSee('Up to 90% Marketplace revenue'); + $response->assertSee('Exclusive discounts'); + $response->assertSee('Direct repo access on GitHub'); + $response->assertSee('Shape the roadmap'); + } + + public function test_benefits_page_uses_config_values(): void + { + $user = $this->createUltraSubscriber(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $includedSeats = config('subscriptions.plans.max.included_seats'); + $extraSeatMonthly = config('subscriptions.plans.max.extra_seat_price_monthly'); + $extraSeatYearly = config('subscriptions.plans.max.extra_seat_price_yearly'); + + $response->assertSee("{$includedSeats} seats included"); + $response->assertSee("\${$extraSeatMonthly}/mo"); + $response->assertSee("\${$extraSeatYearly}/mo on annual"); + } +} diff --git a/tests/Feature/UltraPluginAccessTest.php b/tests/Feature/UltraPluginAccessTest.php index f247235b9..4e4509d25 100644 --- a/tests/Feature/UltraPluginAccessTest.php +++ b/tests/Feature/UltraPluginAccessTest.php @@ -122,9 +122,9 @@ private function createThirdPartyPlugin(): Plugin return $plugin; } - // ---- Phase 1: Free official plugins for Ultra ---- + // ---- Phase 1: Official plugin pricing for Ultra ---- - public function test_ultra_user_gets_zero_price_for_official_plugin(): void + public function test_ultra_user_gets_subscriber_price_for_official_plugin(): void { $user = User::factory()->create(); $this->createPaidMaxSubscription($user); @@ -133,7 +133,7 @@ public function test_ultra_user_gets_zero_price_for_official_plugin(): void $bestPrice = $plugin->getBestPriceForUser($user); $this->assertNotNull($bestPrice); - $this->assertEquals(0, $bestPrice->amount); + $this->assertEquals(1999, $bestPrice->amount); } public function test_ultra_user_gets_regular_price_for_third_party_plugin(): void @@ -181,9 +181,9 @@ public function test_guest_gets_regular_price_for_official_plugin(): void $this->assertEquals(2999, $bestPrice->amount); } - // ---- Phase 2: $0 cart checkout ---- + // ---- Phase 2: Cart captures real price for Ultra users ---- - public function test_zero_cart_skips_stripe_and_creates_licenses(): void + public function test_ultra_user_cart_captures_real_price_for_official_plugin(): void { $user = User::factory()->create(); $this->createPaidMaxSubscription($user); @@ -195,56 +195,9 @@ public function test_zero_cart_skips_stripe_and_creates_licenses(): void $cart = $cartService->getCart($user); $cartService->addPlugin($cart, $plugin); - // Verify the price was captured as $0 + // Verify the price was captured at the subscriber rate, not $0 $cartItem = $cart->items()->first(); - $this->assertEquals(0, $cartItem->price_at_addition); - - // Checkout should skip Stripe and redirect to free success - $response = $this->post(route('cart.checkout')); - $response->assertRedirect(route('cart.success', ['free' => 1])); - - // License should have been created - $this->assertTrue( - $user->pluginLicenses()->forPlugin($plugin)->active()->exists() - ); - - // Cart should be completed - $this->assertNotNull($cart->fresh()->completed_at); - } - - public function test_free_checkout_does_not_create_duplicate_licenses(): void - { - $user = User::factory()->create(); - $this->createPaidMaxSubscription($user); - $plugin = $this->createOfficialPlugin(); - - // Pre-existing license - PluginLicense::factory()->create([ - 'user_id' => $user->id, - 'plugin_id' => $plugin->id, - 'expires_at' => null, - ]); - - $this->actingAs($user); - - $cartService = resolve(CartService::class); - $cart = $cartService->getCart($user); - $cartService->addPlugin($cart, $plugin); - - $this->post(route('cart.checkout')); - - // Should still only have 1 license - $this->assertEquals(1, $user->pluginLicenses()->forPlugin($plugin)->count()); - } - - public function test_free_checkout_success_page_renders(): void - { - $user = User::factory()->create(); - $this->actingAs($user); - - $response = $this->get(route('cart.success', ['free' => 1])); - $response->assertStatus(200); - $response->assertSee('Plugins Added!'); + $this->assertEquals(1999, $cartItem->price_at_addition); } // ---- Phase 3: Team plugin access ---- @@ -731,7 +684,7 @@ public function test_comped_ultra_user_has_ultra_access(): void $this->assertTrue($user->hasUltraAccess()); } - public function test_comped_ultra_user_gets_free_official_plugin(): void + public function test_comped_ultra_user_gets_subscriber_price_for_official_plugin(): void { $user = User::factory()->create(); $this->createCompedUltraSubscription($user); @@ -740,7 +693,7 @@ public function test_comped_ultra_user_gets_free_official_plugin(): void $bestPrice = $plugin->getBestPriceForUser($user); $this->assertNotNull($bestPrice); - $this->assertEquals(0, $bestPrice->amount); + $this->assertEquals(1999, $bestPrice->amount); } public function test_legacy_comped_max_does_not_have_active_ultra_subscription(): void From b5bd301279ebf0254c8b863dbeca177481ae7261 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 30 Mar 2026 12:31:36 +0100 Subject: [PATCH 3/7] Add ad rotation to blog and docs, improve mobile menu UX - Extract blog sidebar ads into shared ad-rotation component with configurable ad filtering - Add Ultra ad to rotation with NativePHP branding and config-driven pricing - Add ad rotation to docs sidebar and mobile view, filtered by platform - Add logout button and shadow to main website mobile menu - Improve menu item contrast in light mode Co-Authored-By: Claude Opus 4.6 --- resources/views/article.blade.php | 11 +- resources/views/blog.blade.php | 11 +- .../components/blog/ad-rotation.blade.php | 162 ++++++++++++++++++ .../views/components/blog/sidebar.blade.php | 107 +----------- .../docs/toc-and-sponsors.blade.php | 4 +- .../components/navbar/mobile-menu.blade.php | 39 +++-- resources/views/docs/index.blade.php | 13 +- 7 files changed, 218 insertions(+), 129 deletions(-) create mode 100644 resources/views/components/blog/ad-rotation.blade.php diff --git a/resources/views/article.blade.php b/resources/views/article.blade.php index 7262e9539..071f8798a 100644 --- a/resources/views/article.blade.php +++ b/resources/views/article.blade.php @@ -102,10 +102,13 @@ class="prose min-w-0 max-w-none grow text-gray-600 dark:text-gray-400 dark:prose

- {{-- Mobile partner card --}} -
- - Become a Partner + {{-- Mobile ad & partner card --}} +
+ +
diff --git a/resources/views/blog.blade.php b/resources/views/blog.blade.php index 94a2b4cf1..4bc25240b 100644 --- a/resources/views/blog.blade.php +++ b/resources/views/blog.blade.php @@ -113,10 +113,13 @@ class="flex grow flex-col gap-5"
- {{-- Mobile partner card --}} -
- - Become a Partner + {{-- Mobile ad & partner card --}} +
+ +
{{-- Pagination --}}