Skip to content

Commit 740c169

Browse files
simonhampclaude
andcommitted
Add EAP pricing on /ultra page, back arrow on license page, and Ultra upsell banner on dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bcbddf1 commit 740c169

File tree

7 files changed

+231
-24
lines changed

7 files changed

+231
-24
lines changed

app/Livewire/MobilePricing.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public function createCheckoutSession(?string $plan, ?User $user = null)
6666
$user->createOrGetStripeCustomer();
6767

6868
$checkout = $user
69-
->newSubscription('default', $subscription->stripePriceId(interval: $this->interval))
69+
->newSubscription('default', $subscription->stripePriceId(forceEap: $user->isEapCustomer(), interval: $this->interval))
7070
->allowPromotionCodes()
7171
->checkout([
7272
'success_url' => $this->successUrl(),
@@ -104,7 +104,7 @@ public function upgradeSubscription(): mixed
104104
return null;
105105
}
106106

107-
$newPriceId = Subscription::Max->stripePriceId(interval: $this->interval);
107+
$newPriceId = Subscription::Max->stripePriceId(forceEap: $user->isEapCustomer(), interval: $this->interval);
108108

109109
$subscription->skipTrial()->swapAndInvoice($newPriceId);
110110

@@ -141,8 +141,21 @@ public function render()
141141
$hasExistingSubscription = false;
142142
$currentPlanName = null;
143143
$isAlreadyUltra = false;
144+
$isEapCustomer = false;
145+
$eapYearlyPrice = null;
146+
$eapDiscountPercent = null;
147+
$eapSavingsVsMonthly = null;
148+
$regularYearlyPrice = config('subscriptions.plans.max.price_yearly');
144149

145150
if ($user = Auth::user()) {
151+
$isEapCustomer = $user->isEapCustomer();
152+
153+
if ($isEapCustomer) {
154+
$eapYearlyPrice = config('subscriptions.plans.max.eap_price_yearly');
155+
$eapDiscountPercent = (int) round((1 - $eapYearlyPrice / $regularYearlyPrice) * 100);
156+
$eapSavingsVsMonthly = (config('subscriptions.plans.max.price_monthly') * 12) - $eapYearlyPrice;
157+
}
158+
146159
$subscription = $user->subscription('default');
147160

148161
if ($subscription && $subscription->active()) {
@@ -163,6 +176,11 @@ public function render()
163176
'hasExistingSubscription' => $hasExistingSubscription,
164177
'currentPlanName' => $currentPlanName,
165178
'isAlreadyUltra' => $isAlreadyUltra,
179+
'isEapCustomer' => $isEapCustomer,
180+
'eapYearlyPrice' => $eapYearlyPrice,
181+
'eapDiscountPercent' => $eapDiscountPercent,
182+
'eapSavingsVsMonthly' => $eapSavingsVsMonthly,
183+
'regularYearlyPrice' => $regularYearlyPrice,
166184
]);
167185
}
168186
}

config/subscriptions.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
'included_seats' => 5,
3535
'extra_seat_price_yearly' => 4,
3636
'extra_seat_price_monthly' => 5,
37+
'price_monthly' => 35,
38+
'price_yearly' => 350,
39+
'eap_price_yearly' => 250,
3740
],
3841
'forever' => [
3942
'name' => 'Forever',

resources/views/livewire/customer/dashboard.blade.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@
2323
</flux:callout>
2424
@endif
2525

26+
{{-- Ultra Upsell Banner --}}
27+
@if(!$this->hasUltraSubscription)
28+
<div class="mb-6 rounded-lg border border-zinc-300 bg-gradient-to-r from-zinc-100 to-zinc-200 p-6 dark:border-zinc-600 dark:from-zinc-800 dark:to-zinc-900">
29+
<div class="flex items-start">
30+
<div class="shrink-0 text-zinc-700 dark:text-zinc-300">
31+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
32+
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 0 0-2.455 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
33+
</svg>
34+
</div>
35+
<div class="ml-4 flex-1">
36+
<h3 class="font-medium text-zinc-900 dark:text-zinc-100">
37+
Upgrade to NativePHP Ultra
38+
</h3>
39+
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
40+
Get all first-party plugins for free, premium support, team management, and more.
41+
</p>
42+
<div class="mt-4">
43+
<a href="{{ route('pricing') }}" class="inline-flex items-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-medium text-white transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:ring-offset-2 dark:bg-white dark:text-black dark:hover:bg-zinc-200">
44+
Learn more
45+
</a>
46+
</div>
47+
</div>
48+
</div>
49+
</div>
50+
@endif
51+
2652
{{-- Banners --}}
2753
<div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
2854
@feature(App\Features\ShowPlugins::class)

resources/views/livewire/customer/licenses/show.blade.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<div>
22
<div class="mb-6">
3-
<flux:heading size="xl">{{ $license->name ?: $license->policy_name }}</flux:heading>
3+
<a href="{{ route('customer.licenses.list') }}" class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400">
4+
<x-heroicon-s-arrow-left class="size-4" />
5+
<span class="font-medium">Licenses</span>
6+
</a>
7+
<flux:heading size="xl" class="mt-4">{{ $license->name ?: $license->policy_name }}</flux:heading>
48
@if($license->name)
59
<flux:text>{{ $license->policy_name }}</flux:text>
610
@endif

resources/views/livewire/mobile-pricing.blade.php

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ class="relative rounded-full px-5 py-2 text-sm font-medium transition"
8686
>
8787
Annual
8888
<span class="absolute -right-2 -top-2.5 rounded-full bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300">
89-
Save 16%
89+
@if($isEapCustomer)
90+
EAP offer
91+
@else
92+
Save 16%
93+
@endif
9094
</span>
9195
</button>
9296
</div>
@@ -124,27 +128,52 @@ class="text-3xl font-semibold"
124128
</h3>
125129

126130
{{-- Price --}}
127-
<div
128-
class="flex items-start gap-1.5 pt-5"
129-
:aria-label="interval === 'month'
130-
? 'Price: $35 per month'
131-
: 'Price: $350 per year'"
132-
>
133-
<div class="text-5xl font-semibold">
134-
$<span x-text="interval === 'month' ? '35' : '350'"></span>
131+
@if($isEapCustomer)
132+
<div class="flex items-start gap-1.5 pt-5">
133+
<div class="text-5xl font-semibold">
134+
$<span x-text="interval === 'month' ? '35' : '{{ $eapYearlyPrice }}'"></span>
135+
</div>
136+
<div class="self-end pb-1.5 text-zinc-500">
137+
<span x-text="interval === 'month' ? 'per month' : 'per year'"></span>
138+
</div>
135139
</div>
136-
<div class="self-end pb-1.5 text-zinc-500">
137-
<span x-text="interval === 'month' ? 'per month' : 'per year'"></span>
140+
<div
141+
x-show="interval === 'year'"
142+
x-transition
143+
class="flex items-center gap-2 pt-1"
144+
>
145+
<span class="text-lg text-zinc-400 line-through">${{ $regularYearlyPrice }}/yr</span>
146+
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300">
147+
{{ $eapDiscountPercent }}% off
148+
</span>
138149
</div>
139-
</div>
150+
@else
151+
<div
152+
class="flex items-start gap-1.5 pt-5"
153+
:aria-label="interval === 'month'
154+
? 'Price: $35 per month'
155+
: 'Price: $350 per year'"
156+
>
157+
<div class="text-5xl font-semibold">
158+
$<span x-text="interval === 'month' ? '35' : '350'"></span>
159+
</div>
160+
<div class="self-end pb-1.5 text-zinc-500">
161+
<span x-text="interval === 'month' ? 'per month' : 'per year'"></span>
162+
</div>
163+
</div>
164+
@endif
140165

141166
{{-- Savings note --}}
142167
<div
143168
x-show="interval === 'year'"
144169
x-transition
145170
class="pt-1 text-sm text-emerald-600 dark:text-emerald-400"
146171
>
147-
Save $70/year vs monthly
172+
@if($isEapCustomer)
173+
Save ${{ $eapSavingsVsMonthly }}/year (compared to monthly pricing) with your Early Access discount
174+
@else
175+
Save $70/year vs monthly
176+
@endif
148177
</div>
149178

150179
{{-- CTA Button --}}
@@ -312,16 +341,31 @@ class="rounded-full px-4 py-1.5 text-sm font-medium transition"
312341
<div class="flex items-baseline justify-between">
313342
<span class="text-sm text-gray-600 dark:text-gray-400">Ultra</span>
314343
<span class="text-lg font-semibold text-gray-900 dark:text-white">
315-
$<span x-text="interval === 'month' ? '35' : '350'"></span><span class="text-sm font-normal text-gray-500">/<span x-text="interval === 'month' ? 'mo' : 'yr'"></span></span>
344+
@if($isEapCustomer)
345+
$<span x-text="interval === 'month' ? '35' : '{{ $eapYearlyPrice }}'"></span><span class="text-sm font-normal text-gray-500">/<span x-text="interval === 'month' ? 'mo' : 'yr'"></span></span>
346+
@else
347+
$<span x-text="interval === 'month' ? '35' : '350'"></span><span class="text-sm font-normal text-gray-500">/<span x-text="interval === 'month' ? 'mo' : 'yr'"></span></span>
348+
@endif
316349
</span>
317350
</div>
318-
<div
319-
x-show="interval === 'year'"
320-
x-transition
321-
class="mt-1 text-xs text-emerald-600 dark:text-emerald-400"
322-
>
323-
Save $70/year vs monthly
324-
</div>
351+
@if($isEapCustomer)
352+
<div
353+
x-show="interval === 'year'"
354+
x-transition
355+
class="mt-1 flex items-center gap-2 text-xs"
356+
>
357+
<span class="text-zinc-400 line-through">${{ $regularYearlyPrice }}/yr</span>
358+
<span class="font-semibold text-emerald-600 dark:text-emerald-400">EAP discount applied</span>
359+
</div>
360+
@else
361+
<div
362+
x-show="interval === 'year'"
363+
x-transition
364+
class="mt-1 text-xs text-emerald-600 dark:text-emerald-400"
365+
>
366+
Save $70/year vs monthly
367+
</div>
368+
@endif
325369
</div>
326370

327371
{{-- Actions --}}

tests/Feature/DashboardLayoutTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,29 @@ public function test_dashboard_hides_team_card_for_non_ultra_user(): void
143143
->assertDontSee('No team yet')
144144
->assertDontSee('Create a team');
145145
}
146+
147+
// ========================================
148+
// Ultra Upsell Banner Tests
149+
// ========================================
150+
151+
public function test_dashboard_shows_ultra_banner_for_non_ultra_user(): void
152+
{
153+
$user = User::factory()->create();
154+
155+
Livewire::actingAs($user)
156+
->test(Dashboard::class)
157+
->assertOk()
158+
->assertSee('Upgrade to NativePHP Ultra')
159+
->assertSee('Learn more');
160+
}
161+
162+
public function test_dashboard_hides_ultra_banner_for_ultra_subscriber(): void
163+
{
164+
$user = $this->createUltraUser();
165+
166+
Livewire::actingAs($user)
167+
->test(Dashboard::class)
168+
->assertOk()
169+
->assertDontSee('Upgrade to NativePHP Ultra');
170+
}
146171
}

tests/Feature/MobilePricingTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tests\Feature;
44

55
use App\Livewire\MobilePricing;
6+
use App\Models\License;
67
use App\Models\User;
78
use Illuminate\Foundation\Testing\RefreshDatabase;
89
use Illuminate\Support\Facades\Auth;
@@ -188,4 +189,90 @@ public function upgrade_modal_not_shown_for_users_without_subscription()
188189
->assertDontSeeHtml('wire:click="upgradeSubscription"')
189190
->assertDontSee('Confirm upgrade');
190191
}
192+
193+
#[Test]
194+
public function eap_customer_sees_eap_offer_badge_on_annual_toggle()
195+
{
196+
$user = User::factory()->create();
197+
License::factory()->eapEligible()->withoutSubscriptionItem()->for($user)->create();
198+
Auth::login($user);
199+
200+
Livewire::test(MobilePricing::class)
201+
->assertSee('EAP offer')
202+
->assertDontSee('Save 16%');
203+
}
204+
205+
#[Test]
206+
public function non_eap_customer_sees_save_badge_on_annual_toggle()
207+
{
208+
$user = User::factory()->create();
209+
Auth::login($user);
210+
211+
Livewire::test(MobilePricing::class)
212+
->assertSee('Save 16%')
213+
->assertDontSee('EAP offer');
214+
}
215+
216+
#[Test]
217+
public function guest_sees_save_badge_on_annual_toggle()
218+
{
219+
Auth::logout();
220+
221+
Livewire::test(MobilePricing::class)
222+
->assertSee('Save 16%')
223+
->assertDontSee('EAP offer');
224+
}
225+
226+
#[Test]
227+
public function eap_customer_sees_strikethrough_and_discounted_price()
228+
{
229+
$user = User::factory()->create();
230+
License::factory()->eapEligible()->withoutSubscriptionItem()->for($user)->create();
231+
Auth::login($user);
232+
233+
$eapPrice = config('subscriptions.plans.max.eap_price_yearly');
234+
$regularPrice = config('subscriptions.plans.max.price_yearly');
235+
$discount = (int) round((1 - $eapPrice / $regularPrice) * 100);
236+
237+
Livewire::test(MobilePricing::class)
238+
->assertSee('$'.$regularPrice.'/yr')
239+
->assertSee($discount.'% off')
240+
->assertSee('Early Access discount');
241+
}
242+
243+
#[Test]
244+
public function non_eap_customer_does_not_see_eap_pricing()
245+
{
246+
$user = User::factory()->create();
247+
License::factory()->afterEap()->withoutSubscriptionItem()->for($user)->create();
248+
Auth::login($user);
249+
250+
Livewire::test(MobilePricing::class)
251+
->assertDontSee('Early Access discount')
252+
->assertDontSee('EAP offer')
253+
->assertSee('Save 16%');
254+
}
255+
256+
#[Test]
257+
public function eap_upgrade_modal_shows_discounted_price()
258+
{
259+
$user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]);
260+
License::factory()->eapEligible()->withoutSubscriptionItem()->for($user)->create();
261+
Auth::login($user);
262+
263+
$subscription = Cashier::$subscriptionModel::factory()
264+
->for($user)
265+
->active()
266+
->create(['stripe_price' => self::PRO_PRICE_ID]);
267+
268+
Cashier::$subscriptionItemModel::factory()
269+
->for($subscription, 'subscription')
270+
->create(['stripe_price' => self::PRO_PRICE_ID]);
271+
272+
$regularPrice = config('subscriptions.plans.max.price_yearly');
273+
274+
Livewire::test(MobilePricing::class)
275+
->assertSee('$'.$regularPrice.'/yr')
276+
->assertSee('EAP discount applied');
277+
}
191278
}

0 commit comments

Comments
 (0)