Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/Http/Controllers/ProductController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class ProductController extends Controller
{
public function show(Product $product): View
public function show(Product $product): View|RedirectResponse
{
abort_unless($product->isActive(), 404);

if ($product->slug === 'nativephp-masterclass') {
return redirect()->route('course');
}

$user = Auth::user();

if (! $product->hasAccessiblePriceFor($user)) {
Expand Down
143 changes: 84 additions & 59 deletions resources/views/course.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,30 +112,39 @@ class="mx-auto mt-6 max-w-2xl text-lg text-gray-600 dark:text-gray-400"
"
class="mt-8 flex flex-col items-center gap-4 sm:flex-row"
>
<a
href="#pricing"
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"
>
Get Early Bird Access &mdash; $101
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
@if ($alreadyOwned)
<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">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
You Own This Course
</div>
@else
<a
href="#pricing"
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"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
</a>
<a
href="#signup"
class="text-sm font-medium text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
or join the waitlist
</a>
Get Early Bird Access &mdash; $101
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
</a>
<a
href="#signup"
class="text-sm font-medium text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
or join the waitlist
</a>
@endif
</div>
</div>
</section>
Expand Down Expand Up @@ -573,14 +582,28 @@ class="mx-auto mt-10 max-w-lg"
The NativePHP Masterclass
</h3>

<div class="mt-4 flex items-baseline gap-2">
<span class="text-5xl font-bold text-gray-900 dark:text-white">
$101
</span>
<span class="text-gray-500 dark:text-gray-400">
one-time payment
</span>
</div>
@if ($alreadyOwned)
<div class="mt-6 text-center">
<div class="mx-auto grid size-14 place-items-center rounded-full bg-emerald-100 dark:bg-emerald-900/50">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</div>
<p class="mt-3 text-lg font-semibold text-emerald-700 dark:text-emerald-300">You Own This Course</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
You'll be notified when the course launches.
</p>
</div>
@else
<div class="mt-4 flex items-baseline gap-2">
<span class="text-5xl font-bold text-gray-900 dark:text-white">
$101
</span>
<span class="text-gray-500 dark:text-gray-400">
one-time payment
</span>
</div>
@endif

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

<form
action="{{ route('course.checkout') }}"
method="POST"
class="mt-8"
id="checkout-form"
>
@csrf
<button
type="submit"
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"
@unless ($alreadyOwned)
<form
action="{{ route('course.checkout') }}"
method="POST"
class="mt-8"
id="checkout-form"
>
Get Early Bird Access
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
@csrf
<button
type="submit"
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"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
</button>
</form>
Get Early Bird Access
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
</button>
</form>

<p class="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
Early bird pricing won't last forever. Lock in the lowest price today.
</p>
<p class="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
Early bird pricing won't last forever. Lock in the lowest price today.
</p>
@endunless
</div>
</div>
</section>
Expand Down Expand Up @@ -755,7 +780,7 @@ class="rounded-xl bg-gray-900 px-6 py-3 text-sm font-semibold text-white transit
</div>

@auth
@if (request('checkout'))
@if (request('checkout') && ! $alreadyOwned)
<script>
document.getElementById('checkout-form').submit();
</script>
Expand Down
10 changes: 9 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@
Route::view('/', 'welcome')->name('welcome');
Route::redirect('pricing', 'blog/nativephp-for-mobile-is-now-free')->name('pricing');
Route::view('alt-pricing', 'alt-pricing')->name('alt-pricing')->middleware('signed');
Route::view('course', 'course')->name('course');
Route::get('course', function () {
$user = auth()->user();
$product = \App\Models\Product::where('slug', 'nativephp-masterclass')->first();
$alreadyOwned = $user && $product && $product->isOwnedBy($user);

return view('course', [
'alreadyOwned' => $alreadyOwned,
]);
})->name('course');

Route::post('course/checkout', function (\Illuminate\Http\Request $request) {
$user = $request->user();
Expand Down
87 changes: 87 additions & 0 deletions tests/Feature/ProductPageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace Tests\Feature;

use App\Models\Product;
use App\Models\ProductLicense;
use App\Models\ProductPrice;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class ProductPageTest extends TestCase
{
use RefreshDatabase;

#[Test]
public function masterclass_product_page_redirects_to_course(): void
{
$product = Product::where('slug', 'nativephp-masterclass')->first();

$this
->get(route('products.show', $product))
->assertRedirect(route('course'));
}

#[Test]
public function non_masterclass_product_page_loads_normally(): void
{
$product = Product::factory()->active()->create([
'slug' => 'plugin-dev-kit',
]);

ProductPrice::factory()->for($product)->create();

$this
->withoutVite()
->get(route('products.show', $product))
->assertStatus(200)
->assertSee($product->name);
}

#[Test]
public function course_page_shows_purchase_form_for_guests(): void
{
$this
->withoutVite()
->get(route('course'))
->assertStatus(200)
->assertSee('Get Early Bird Access')
->assertDontSee('You Own This Course');
}

#[Test]
public function course_page_shows_purchase_form_for_users_without_purchase(): void
{
$user = User::factory()->create();

$this
->withoutVite()
->actingAs($user)
->get(route('course'))
->assertStatus(200)
->assertSee('Get Early Bird Access')
->assertDontSee('You Own This Course');
}

#[Test]
public function course_page_shows_owned_state_for_purchasers(): void
{
$user = User::factory()->create();
$product = Product::where('slug', 'nativephp-masterclass')->first();

ProductLicense::factory()->create([
'user_id' => $user->id,
'product_id' => $product->id,
]);

$this
->withoutVite()
->actingAs($user)
->get(route('course'))
->assertStatus(200)
->assertSee('You Own This Course')
->assertDontSee('Get Early Bird Access');
}
}