Skip to content

Commit d65ef9c

Browse files
simonhampclaude
andauthored
Redirect masterclass product page to /course and show ownership state (#279)
The /products/nativephp-masterclass route now redirects to /course. The course page checks if the logged-in user already owns the product and shows a "You Own This Course" state instead of the purchase form. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ff77694 commit d65ef9c

4 files changed

Lines changed: 186 additions & 61 deletions

File tree

app/Http/Controllers/ProductController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
namespace App\Http\Controllers;
44

55
use App\Models\Product;
6+
use Illuminate\Http\RedirectResponse;
67
use Illuminate\Support\Facades\Auth;
78
use Illuminate\View\View;
89

910
class ProductController extends Controller
1011
{
11-
public function show(Product $product): View
12+
public function show(Product $product): View|RedirectResponse
1213
{
1314
abort_unless($product->isActive(), 404);
1415

16+
if ($product->slug === 'nativephp-masterclass') {
17+
return redirect()->route('course');
18+
}
19+
1520
$user = Auth::user();
1621

1722
if (! $product->hasAccessiblePriceFor($user)) {

resources/views/course.blade.php

Lines changed: 84 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -112,30 +112,39 @@ class="mx-auto mt-6 max-w-2xl text-lg text-gray-600 dark:text-gray-400"
112112
"
113113
class="mt-8 flex flex-col items-center gap-4 sm:flex-row"
114114
>
115-
<a
116-
href="#pricing"
117-
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"
118-
>
119-
Get Early Bird Access &mdash; $101
120-
<svg
121-
xmlns="http://www.w3.org/2000/svg"
122-
viewBox="0 0 20 20"
123-
fill="currentColor"
124-
class="size-5"
115+
@if ($alreadyOwned)
116+
<div class="inline-flex items-center gap-2 rounded-xl bg-emerald-100 px-8 py-4 font-semibold text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">
117+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-5">
118+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
119+
</svg>
120+
You Own This Course
121+
</div>
122+
@else
123+
<a
124+
href="#pricing"
125+
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"
125126
>
126-
<path
127-
fill-rule="evenodd"
128-
d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z"
129-
clip-rule="evenodd"
130-
/>
131-
</svg>
132-
</a>
133-
<a
134-
href="#signup"
135-
class="text-sm font-medium text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
136-
>
137-
or join the waitlist
138-
</a>
127+
Get Early Bird Access &mdash; $101
128+
<svg
129+
xmlns="http://www.w3.org/2000/svg"
130+
viewBox="0 0 20 20"
131+
fill="currentColor"
132+
class="size-5"
133+
>
134+
<path
135+
fill-rule="evenodd"
136+
d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z"
137+
clip-rule="evenodd"
138+
/>
139+
</svg>
140+
</a>
141+
<a
142+
href="#signup"
143+
class="text-sm font-medium text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
144+
>
145+
or join the waitlist
146+
</a>
147+
@endif
139148
</div>
140149
</div>
141150
</section>
@@ -573,14 +582,28 @@ class="mx-auto mt-10 max-w-lg"
573582
The NativePHP Masterclass
574583
</h3>
575584

576-
<div class="mt-4 flex items-baseline gap-2">
577-
<span class="text-5xl font-bold text-gray-900 dark:text-white">
578-
$101
579-
</span>
580-
<span class="text-gray-500 dark:text-gray-400">
581-
one-time payment
582-
</span>
583-
</div>
585+
@if ($alreadyOwned)
586+
<div class="mt-6 text-center">
587+
<div class="mx-auto grid size-14 place-items-center rounded-full bg-emerald-100 dark:bg-emerald-900/50">
588+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-7 text-emerald-600 dark:text-emerald-400">
589+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
590+
</svg>
591+
</div>
592+
<p class="mt-3 text-lg font-semibold text-emerald-700 dark:text-emerald-300">You Own This Course</p>
593+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
594+
You'll be notified when the course launches.
595+
</p>
596+
</div>
597+
@else
598+
<div class="mt-4 flex items-baseline gap-2">
599+
<span class="text-5xl font-bold text-gray-900 dark:text-white">
600+
$101
601+
</span>
602+
<span class="text-gray-500 dark:text-gray-400">
603+
one-time payment
604+
</span>
605+
</div>
606+
@endif
584607

585608
<ul class="mt-8 space-y-3">
586609
<li class="flex items-center gap-3">
@@ -605,36 +628,38 @@ class="mx-auto mt-10 max-w-lg"
605628
</li>
606629
</ul>
607630

608-
<form
609-
action="{{ route('course.checkout') }}"
610-
method="POST"
611-
class="mt-8"
612-
id="checkout-form"
613-
>
614-
@csrf
615-
<button
616-
type="submit"
617-
class="flex w-full items-center justify-center gap-2 rounded-xl bg-emerald-600 px-8 py-4 text-center font-semibold text-white transition hover:bg-emerald-700"
631+
@unless ($alreadyOwned)
632+
<form
633+
action="{{ route('course.checkout') }}"
634+
method="POST"
635+
class="mt-8"
636+
id="checkout-form"
618637
>
619-
Get Early Bird Access
620-
<svg
621-
xmlns="http://www.w3.org/2000/svg"
622-
viewBox="0 0 20 20"
623-
fill="currentColor"
624-
class="size-5"
638+
@csrf
639+
<button
640+
type="submit"
641+
class="flex w-full items-center justify-center gap-2 rounded-xl bg-emerald-600 px-8 py-4 text-center font-semibold text-white transition hover:bg-emerald-700"
625642
>
626-
<path
627-
fill-rule="evenodd"
628-
d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z"
629-
clip-rule="evenodd"
630-
/>
631-
</svg>
632-
</button>
633-
</form>
643+
Get Early Bird Access
644+
<svg
645+
xmlns="http://www.w3.org/2000/svg"
646+
viewBox="0 0 20 20"
647+
fill="currentColor"
648+
class="size-5"
649+
>
650+
<path
651+
fill-rule="evenodd"
652+
d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z"
653+
clip-rule="evenodd"
654+
/>
655+
</svg>
656+
</button>
657+
</form>
634658

635-
<p class="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
636-
Early bird pricing won't last forever. Lock in the lowest price today.
637-
</p>
659+
<p class="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
660+
Early bird pricing won't last forever. Lock in the lowest price today.
661+
</p>
662+
@endunless
638663
</div>
639664
</div>
640665
</section>
@@ -755,7 +780,7 @@ class="rounded-xl bg-gray-900 px-6 py-3 text-sm font-semibold text-white transit
755780
</div>
756781

757782
@auth
758-
@if (request('checkout'))
783+
@if (request('checkout') && ! $alreadyOwned)
759784
<script>
760785
document.getElementById('checkout-form').submit();
761786
</script>

routes/web.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@
6464
Route::view('/', 'welcome')->name('welcome');
6565
Route::redirect('pricing', 'blog/nativephp-for-mobile-is-now-free')->name('pricing');
6666
Route::view('alt-pricing', 'alt-pricing')->name('alt-pricing')->middleware('signed');
67-
Route::view('course', 'course')->name('course');
67+
Route::get('course', function () {
68+
$user = auth()->user();
69+
$product = \App\Models\Product::where('slug', 'nativephp-masterclass')->first();
70+
$alreadyOwned = $user && $product && $product->isOwnedBy($user);
71+
72+
return view('course', [
73+
'alreadyOwned' => $alreadyOwned,
74+
]);
75+
})->name('course');
6876

6977
Route::post('course/checkout', function (\Illuminate\Http\Request $request) {
7078
$user = $request->user();

tests/Feature/ProductPageTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Models\Product;
6+
use App\Models\ProductLicense;
7+
use App\Models\ProductPrice;
8+
use App\Models\User;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use PHPUnit\Framework\Attributes\Test;
11+
use Tests\TestCase;
12+
13+
class ProductPageTest extends TestCase
14+
{
15+
use RefreshDatabase;
16+
17+
#[Test]
18+
public function masterclass_product_page_redirects_to_course(): void
19+
{
20+
$product = Product::where('slug', 'nativephp-masterclass')->first();
21+
22+
$this
23+
->get(route('products.show', $product))
24+
->assertRedirect(route('course'));
25+
}
26+
27+
#[Test]
28+
public function non_masterclass_product_page_loads_normally(): void
29+
{
30+
$product = Product::factory()->active()->create([
31+
'slug' => 'plugin-dev-kit',
32+
]);
33+
34+
ProductPrice::factory()->for($product)->create();
35+
36+
$this
37+
->withoutVite()
38+
->get(route('products.show', $product))
39+
->assertStatus(200)
40+
->assertSee($product->name);
41+
}
42+
43+
#[Test]
44+
public function course_page_shows_purchase_form_for_guests(): void
45+
{
46+
$this
47+
->withoutVite()
48+
->get(route('course'))
49+
->assertStatus(200)
50+
->assertSee('Get Early Bird Access')
51+
->assertDontSee('You Own This Course');
52+
}
53+
54+
#[Test]
55+
public function course_page_shows_purchase_form_for_users_without_purchase(): void
56+
{
57+
$user = User::factory()->create();
58+
59+
$this
60+
->withoutVite()
61+
->actingAs($user)
62+
->get(route('course'))
63+
->assertStatus(200)
64+
->assertSee('Get Early Bird Access')
65+
->assertDontSee('You Own This Course');
66+
}
67+
68+
#[Test]
69+
public function course_page_shows_owned_state_for_purchasers(): void
70+
{
71+
$user = User::factory()->create();
72+
$product = Product::where('slug', 'nativephp-masterclass')->first();
73+
74+
ProductLicense::factory()->create([
75+
'user_id' => $user->id,
76+
'product_id' => $product->id,
77+
]);
78+
79+
$this
80+
->withoutVite()
81+
->actingAs($user)
82+
->get(route('course'))
83+
->assertStatus(200)
84+
->assertSee('You Own This Course')
85+
->assertDontSee('Get Early Bird Access');
86+
}
87+
}

0 commit comments

Comments
 (0)