Skip to content

Commit 6f43d44

Browse files
simonhampclaude
andcommitted
Increase Masterclass price to $199 with countdown to $299
Raise the Masterclass price to $199 immediately and show a countdown to the $299 increase at 2026-06-15 00:00 UTC. After the deadline the countdown hides and $299 pricing is shown. Adds STRIPE_COURSE_PRICE_ID_199 and STRIPE_COURSE_PRICE_ID_299 env keys and a shared deadline config value, a reusable countdown component, and surfaces the countdown on the public course page, the dashboard course page, and as a card on the main dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3591e6c commit 6f43d44

13 files changed

Lines changed: 407 additions & 23 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ STRIPE_FOREVER_PAYMENT_LINK=
8181
STRIPE_TRIAL_PAYMENT_LINK=
8282

8383
STRIPE_COURSE_PRICE_ID=
84+
STRIPE_COURSE_PRICE_ID_199=
85+
STRIPE_COURSE_PRICE_ID_299=
8486

8587
ANYSTACK_API_KEY=
8688
ANYSTACK_PRODUCT_ID=

app/Livewire/Customer/Course/Index.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ public function hasPurchased(): bool
3636
return $product && $product->isOwnedBy(auth()->user());
3737
}
3838

39+
#[Computed]
40+
public function priceIncreaseAt(): string
41+
{
42+
return config('services.stripe.course_price_increase_at');
43+
}
44+
45+
#[Computed]
46+
public function priceIncreased(): bool
47+
{
48+
return now()->gte($this->priceIncreaseAt());
49+
}
50+
51+
#[Computed]
52+
public function currentPrice(): int
53+
{
54+
return $this->priceIncreased() ? 299 : 199;
55+
}
56+
3957
#[Computed]
4058
public function completedLessonIds(): array
4159
{

app/Livewire/Customer/Dashboard.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Livewire\Customer;
44

55
use App\Enums\Subscription;
6+
use App\Models\Product;
67
use App\Models\Team;
78
use Livewire\Attributes\Computed;
89
use Livewire\Attributes\Layout;
@@ -128,4 +129,24 @@ public function totalPurchases(): int
128129
+ $this->pluginLicenseCount
129130
+ $user->productLicenses()->count();
130131
}
132+
133+
#[Computed]
134+
public function hasPurchasedCourse(): bool
135+
{
136+
$product = Product::where('slug', 'nativephp-masterclass')->first();
137+
138+
return $product && $product->isOwnedBy(auth()->user());
139+
}
140+
141+
#[Computed]
142+
public function priceIncreaseAt(): string
143+
{
144+
return config('services.stripe.course_price_increase_at');
145+
}
146+
147+
#[Computed]
148+
public function priceIncreased(): bool
149+
{
150+
return now()->gte($this->priceIncreaseAt());
151+
}
131152
}

config/services.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@
7272

7373
'stripe' => [
7474
'course_price_id' => env('STRIPE_COURSE_PRICE_ID'),
75+
'course_price_id_199' => env('STRIPE_COURSE_PRICE_ID_199'),
76+
'course_price_id_299' => env('STRIPE_COURSE_PRICE_ID_299'),
77+
'course_price_increase_at' => '2026-06-15T00:00:00Z',
7578
],
7679

7780
'stripe_connect' => [

resources/css/app.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,16 @@ nav.docs-navigation li:has(.third-tier .exact-active) > .subsection-header {
519519
}
520520
}
521521

522+
/*
523+
Let the course purchase page fill the entire main area (the gradient hero
524+
should bleed edge to edge), overriding flux:main's centered max-w-7xl
525+
container. Inner content re-applies its own max-width and padding.
526+
*/
527+
[data-flux-main]:has(.course-fullbleed) {
528+
max-width: none;
529+
padding: 0;
530+
}
531+
522532
/* Snippet component with tabbed code blocks */
523533
.snippet {
524534
@apply rounded-xl shadow-lg;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
@props([
2+
'deadline',
3+
'expired' => false,
4+
'heading' => 'Price increases to $299 in',
5+
])
6+
7+
<div
8+
x-data="{
9+
deadline: new Date('{{ $deadline }}').getTime(),
10+
expired: {{ $expired ? 'true' : 'false' }},
11+
days: 0,
12+
hours: 0,
13+
minutes: 0,
14+
seconds: 0,
15+
tick() {
16+
const diff = this.deadline - Date.now()
17+
18+
if (diff <= 0) {
19+
this.expired = true
20+
return
21+
}
22+
23+
this.days = Math.floor(diff / 86400000)
24+
this.hours = Math.floor((diff % 86400000) / 3600000)
25+
this.minutes = Math.floor((diff % 3600000) / 60000)
26+
this.seconds = Math.floor((diff % 60000) / 1000)
27+
},
28+
init() {
29+
this.tick()
30+
setInterval(() => this.tick(), 1000)
31+
},
32+
}"
33+
x-show="!expired"
34+
x-cloak
35+
{{ $attributes->merge(['class' => 'rounded-xl bg-white/60 p-4 dark:bg-white/5']) }}
36+
>
37+
<p class="text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">{{ $heading }}</p>
38+
<div class="mt-2 grid grid-cols-4 gap-2 text-center">
39+
<div>
40+
<span class="block text-2xl font-bold text-gray-900 tabular-nums dark:text-white" x-text="days">0</span>
41+
<span class="text-xs text-gray-500 dark:text-gray-400">Days</span>
42+
</div>
43+
<div>
44+
<span class="block text-2xl font-bold text-gray-900 tabular-nums dark:text-white" x-text="hours">0</span>
45+
<span class="text-xs text-gray-500 dark:text-gray-400">Hours</span>
46+
</div>
47+
<div>
48+
<span class="block text-2xl font-bold text-gray-900 tabular-nums dark:text-white" x-text="minutes">0</span>
49+
<span class="text-xs text-gray-500 dark:text-gray-400">Mins</span>
50+
</div>
51+
<div>
52+
<span class="block text-2xl font-bold text-gray-900 tabular-nums dark:text-white" x-text="seconds">0</span>
53+
<span class="text-xs text-gray-500 dark:text-gray-400">Secs</span>
54+
</div>
55+
</div>
56+
</div>

resources/views/course.blade.php

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ class="inline-flex items-center gap-2 rounded-full bg-emerald-100 px-4 py-1.5 te
3131
<span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
3232
<span class="relative inline-flex size-2 rounded-full bg-emerald-500"></span>
3333
</span>
34-
Early Bird Pricing Available
34+
@if ($priceIncreased)
35+
Now Available
36+
@else
37+
Early Bird Pricing Available
38+
@endif
3539
</div>
3640

3741
{{-- Title --}}
@@ -124,7 +128,7 @@ class="mt-8 flex flex-col items-center gap-4 sm:flex-row"
124128
href="#pricing"
125129
class="inline-flex items-center gap-2 rounded-xl bg-emerald-600 px-8 py-4 font-semibold text-white transition hover:bg-emerald-700"
126130
>
127-
Get Early Bird Access &mdash; $101
131+
Get Access &mdash; ${{ $currentPrice }}
128132
<svg
129133
xmlns="http://www.w3.org/2000/svg"
130134
viewBox="0 0 20 20"
@@ -146,6 +150,14 @@ class="text-sm font-medium text-gray-500 transition hover:text-gray-700 dark:tex
146150
</a>
147151
@endif
148152
</div>
153+
154+
@unless ($alreadyOwned)
155+
<x-course.countdown
156+
:deadline="$priceIncreaseAt"
157+
:expired="$priceIncreased"
158+
class="mx-auto mt-8 w-full max-w-sm rounded-xl bg-gray-100 p-4 dark:bg-mirage"
159+
/>
160+
@endunless
149161
</div>
150162
</section>
151163

@@ -504,8 +516,23 @@ class="mx-auto mt-10 grid max-w-3xl gap-6 sm:grid-cols-2"
504516
</div>
505517

506518
{{-- Pro Tier --}}
507-
<div class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-emerald-50 to-teal-50 p-8 ring-2 ring-emerald-300 dark:from-emerald-950/40 dark:to-teal-950/40 dark:ring-emerald-700">
508-
<div class="absolute right-6 top-6 rounded-full bg-emerald-600 px-3 py-1 text-xs font-bold text-white">
519+
<div
520+
class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-emerald-50 to-teal-50 p-8 ring-2 ring-emerald-300 dark:from-emerald-950/40 dark:to-teal-950/40 dark:ring-emerald-700"
521+
x-data="{
522+
deadline: new Date('{{ $priceIncreaseAt }}').getTime(),
523+
expired: {{ $priceIncreased ? 'true' : 'false' }},
524+
init() {
525+
const check = () => {
526+
if (Date.now() >= this.deadline) {
527+
this.expired = true
528+
}
529+
}
530+
check()
531+
setInterval(check, 1000)
532+
},
533+
}"
534+
>
535+
<div x-show="!expired" class="absolute right-6 top-6 rounded-full bg-emerald-600 px-3 py-1 text-xs font-bold text-white">
509536
EARLY BIRD
510537
</div>
511538

@@ -522,12 +549,30 @@ class="mx-auto mt-10 grid max-w-3xl gap-6 sm:grid-cols-2"
522549
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">You have full access to all content.</p>
523550
</div>
524551
@else
525-
<div class="mt-4 flex items-baseline gap-2">
526-
<span class="text-5xl font-bold text-gray-900 dark:text-white">$101</span>
527-
<span class="text-2xl text-gray-400 line-through">$299</span>
528-
<span class="text-sm text-gray-500 dark:text-gray-400">one-time</span>
552+
{{-- $199 pricing (before deadline) --}}
553+
<div x-show="!expired" x-cloak>
554+
<div class="mt-4 flex items-baseline gap-2">
555+
<span class="text-5xl font-bold text-gray-900 dark:text-white">$199</span>
556+
<span class="text-2xl text-gray-400 line-through">$299</span>
557+
<span class="text-sm text-gray-500 dark:text-gray-400">one-time</span>
558+
</div>
559+
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Early bird pricing. Lock it in now &mdash; full access forever.</p>
560+
561+
<x-course.countdown
562+
:deadline="$priceIncreaseAt"
563+
:expired="$priceIncreased"
564+
class="mt-4 rounded-xl bg-white/60 p-4 dark:bg-white/5"
565+
/>
566+
</div>
567+
568+
{{-- $299 pricing (after deadline) --}}
569+
<div x-show="expired" x-cloak>
570+
<div class="mt-4 flex items-baseline gap-2">
571+
<span class="text-5xl font-bold text-gray-900 dark:text-white">$299</span>
572+
<span class="text-sm text-gray-500 dark:text-gray-400">one-time</span>
573+
</div>
574+
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">One price. Full access forever.</p>
529575
</div>
530-
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Early bird pricing. Lock it in now &mdash; full access forever.</p>
531576
@endif
532577

533578
<ul class="mt-8 space-y-3">
@@ -558,7 +603,7 @@ class="flex w-full items-center justify-center gap-2 rounded-xl bg-emerald-600 p
558603
</button>
559604
</form>
560605

561-
<p class="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
606+
<p x-show="!expired" class="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
562607
Early bird pricing won't last forever. Lock in the lowest price today.
563608
</p>
564609
@endunless

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,20 @@ class="h-2 rounded-full bg-emerald-500 transition-all"
9797
@endif
9898
@else
9999
{{-- Not purchased: Full-width marketing/purchase page --}}
100-
<div class="-mx-6 -mt-6 sm:-mx-8 sm:-mt-8">
100+
<div class="course-fullbleed">
101101
{{-- Hero --}}
102-
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-b from-violet-50 to-white px-6 py-16 sm:px-12 sm:py-20 dark:from-zinc-900 dark:to-zinc-950">
102+
<div class="relative overflow-hidden bg-gradient-to-b from-violet-50 to-white px-6 py-16 sm:px-12 sm:py-20 dark:from-zinc-900 dark:to-zinc-950">
103103
{{-- Background glow --}}
104104
<div class="pointer-events-none absolute -top-24 left-1/2 size-[500px] -translate-x-1/2 rounded-full bg-violet-500/5 blur-[120px] dark:bg-violet-500/10" aria-hidden="true"></div>
105105
<div class="pointer-events-none absolute -bottom-32 -right-32 size-[400px] rounded-full bg-indigo-500/5 blur-[100px] dark:bg-indigo-500/10" aria-hidden="true"></div>
106106

107107
<div class="relative z-10 mx-auto max-w-2xl text-center">
108108
<span class="inline-flex items-center gap-2 rounded-md bg-violet-500/10 px-3 py-1 text-xs font-bold uppercase tracking-widest text-violet-600 ring-1 ring-violet-500/20 dark:text-violet-400">
109-
New Course &mdash; Early Bird
109+
@if ($this->priceIncreased)
110+
New Course
111+
@else
112+
New Course &mdash; Early Bird
113+
@endif
110114
</span>
111115

112116
<h1 class="mt-8 text-4xl font-black tracking-tight text-zinc-900 sm:text-5xl lg:text-6xl dark:text-white">
@@ -135,17 +139,30 @@ class="h-2 rounded-full bg-emerald-500 transition-all"
135139

136140
{{-- Price --}}
137141
<div class="mt-10 flex items-baseline justify-center gap-3">
138-
<span class="text-5xl font-black text-zinc-900 dark:text-white">$101</span>
139-
<span class="text-xl text-zinc-400 line-through dark:text-zinc-600">$299</span>
142+
<span class="text-5xl font-black text-zinc-900 dark:text-white">${{ $this->currentPrice }}</span>
143+
@unless ($this->priceIncreased)
144+
<span class="text-xl text-zinc-400 line-through dark:text-zinc-600">$299</span>
145+
@endunless
140146
<span class="text-sm text-zinc-500">one-time</span>
141147
</div>
142148

149+
{{-- Countdown --}}
150+
<x-course.countdown
151+
:deadline="$this->priceIncreaseAt"
152+
:expired="$this->priceIncreased"
153+
class="mx-auto mt-8 w-full max-w-sm rounded-xl bg-white/70 p-4 ring-1 ring-zinc-200 dark:bg-white/5 dark:ring-white/10"
154+
/>
155+
143156
{{-- CTA --}}
144157
<div class="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
145158
<form action="{{ route('course.checkout') }}" method="POST">
146159
@csrf
147160
<button type="submit" class="inline-flex items-center gap-2 rounded-xl bg-gradient-to-b from-violet-500 to-violet-600 px-8 py-3.5 text-sm font-semibold text-white shadow-lg shadow-violet-500/25 ring-1 ring-violet-400/20 transition hover:shadow-xl hover:shadow-violet-500/30">
148-
Get Early Bird Access
161+
@if ($this->priceIncreased)
162+
Get Access
163+
@else
164+
Get Early Bird Access
165+
@endif
149166
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>
150167
</button>
151168
</form>
@@ -157,7 +174,7 @@ class="h-2 rounded-full bg-emerald-500 transition-all"
157174
</div>
158175

159176
{{-- What's Included --}}
160-
<div class="mt-8 grid gap-4 px-1 sm:grid-cols-2 lg:grid-cols-4">
177+
<div class="mx-auto mt-8 grid max-w-7xl gap-4 px-6 pb-6 sm:grid-cols-2 lg:grid-cols-4 lg:px-8 lg:pb-8">
161178
<div class="rounded-xl border border-zinc-200 bg-white p-5 dark:border-white/10 dark:bg-white/5">
162179
<div class="flex size-9 items-center justify-center rounded-lg bg-violet-100 dark:bg-violet-500/15">
163180
<x-heroicon-s-device-phone-mobile class="size-4 text-violet-600 dark:text-violet-400" />

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,41 @@
4343
</flux:callout>
4444
@endif
4545

46+
{{-- Masterclass Countdown Banner --}}
47+
@feature(App\Features\ShowMasterclass::class)
48+
@if(!$this->hasPurchasedCourse && !$this->priceIncreased)
49+
<div class="mb-6 overflow-hidden rounded-lg border border-violet-200 bg-gradient-to-br from-violet-50 to-white p-6 dark:border-violet-700/50 dark:from-violet-950/30 dark:to-zinc-900">
50+
<div class="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
51+
<div class="flex items-start gap-4">
52+
<div class="shrink-0 text-violet-600 dark:text-violet-400">
53+
<x-heroicon-s-academic-cap class="size-6" />
54+
</div>
55+
<div>
56+
<h3 class="font-medium text-violet-900 dark:text-violet-100">
57+
The NativePHP Masterclass
58+
</h3>
59+
<p class="mt-1 text-sm text-violet-700 dark:text-violet-300">
60+
Lock in early bird pricing &mdash; <span class="font-semibold">$199</span> now, before it goes up to $299.
61+
</p>
62+
<div class="mt-4">
63+
<a href="{{ route('customer.course.index') }}" class="inline-flex items-center gap-2 rounded-md bg-violet-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-violet-700">
64+
Get the course
65+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>
66+
</a>
67+
</div>
68+
</div>
69+
</div>
70+
71+
<x-course.countdown
72+
:deadline="$this->priceIncreaseAt"
73+
:expired="$this->priceIncreased"
74+
class="w-full shrink-0 rounded-xl bg-white/70 p-4 ring-1 ring-violet-200 sm:w-auto sm:min-w-72 dark:bg-white/5 dark:ring-white/10"
75+
/>
76+
</div>
77+
</div>
78+
@endif
79+
@endfeature
80+
4681
{{-- Ultra Upsell Banner --}}
4782
@if(!$this->hasUltraSubscription)
4883
<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">

routes/web.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,15 @@
102102
}])
103103
->first();
104104

105+
$priceIncreaseAt = config('services.stripe.course_price_increase_at');
106+
$priceIncreased = now()->gte($priceIncreaseAt);
107+
105108
return view('course', [
106109
'alreadyOwned' => $alreadyOwned,
107110
'course' => $course,
111+
'priceIncreaseAt' => $priceIncreaseAt,
112+
'priceIncreased' => $priceIncreased,
113+
'currentPrice' => $priceIncreased ? 299 : 199,
108114
]);
109115
})->name('course');
110116

@@ -124,7 +130,10 @@
124130
return to_route('course')->with('error', 'You already own this course.');
125131
}
126132

127-
$priceId = config('services.stripe.course_price_id');
133+
$priceIncreased = now()->gte(config('services.stripe.course_price_increase_at'));
134+
$priceId = $priceIncreased
135+
? config('services.stripe.course_price_id_299')
136+
: config('services.stripe.course_price_id_199');
128137

129138
if (! $priceId) {
130139
return to_route('course')->with('error', 'Course checkout is not configured yet.');

0 commit comments

Comments
 (0)