8000 V2 purchase flow (#139) · rinodrummer/nativephp.com@e08c5a1 · GitHub
[go: up one dir, main page]

Skip to content

Commit e08c5a1

Browse files
authored
V2 purchase flow (NativePHP#139)
* collect email for guest users and create acct before purchase * change input style and auto focus * cleanup * remove comment
1 parent 186fcd2 commit e08c5a1

File tree

8 files changed

+439
-48
lines changed
< 10000 div class="prc-PageLayout-VerticalDivider-4A4Qm prc-PageLayout-PaneVerticalDivider-1c9vy" data-variant="none" data-position="start" style="--spacing:var(--spacing-none)">

8 files changed

+439
-48
lines changed

app/Livewire/MobilePricing.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use App\Enums\Subscription;
6+
use App\Models\User;
7+
use Illuminate\Support\Facades\Auth;
8+
use Illuminate\Support\Facades\Hash;
9+
use Illuminate\Support\Str;
10+
use Livewire\Component;
11+
12+
class MobilePricing extends Component
13+
{
14+
protected $listeners = [
15+
'purchase-request-submitted' => 'handlePurchaseRequest',
16+
];
17+
18+
public function handlePurchaseRequest(array $data)
19+
{
20+
$user = $this->findOrCreateUser($data['email']);
21+
22+
return $this->createCheckoutSession($data['plan'], $user);
23+
}
24+
25+
public function createCheckoutSession(string $plan, ?User $user = null)
26+
{
27+
if (! ($user ??= Auth::user())) {
28+
return;
29+
}
30+
31+
if (! ($subscription = Subscription::tryFrom($plan))) {
32+
return;
33+
}
34+
35+
$user->createOrGetStripeCustomer();
36+
37+
$checkout = $user
38+
->newSubscription('default', $subscription->stripePriceId())
39+
->allowPromotionCodes()
40+
->checkout([
41+
'success_url' => $this->successUrl(),
42+
'cancel_url' => route('early-adopter'),
43+
]);
44+
45+
return redirect($checkout->url);
46+
}
47+
48+
private function findOrCreateUser(string $email): User
49+
{
50+
return User::firstOrCreate([
51+
'email' => $email,
52+
], [
53+
'password' => Hash::make(Str::random(72)),
54+
]);
55+
}
56+
57+
private function successUrl(): string
58+
{
59+
// This is a hack to get the route() function to work. If you try
60+
// to pass {CHECKOUT_SESSION_ID} to the route function, it will
61+
// throw an error.
62+
return Str::replace(
63+
'abc',
64+
'{CHECKOUT_SESSION_ID}',
65+
route('order.success', ['checkoutSessionId' => 'abc'])
66+
);
67+
}
68+
69+
public function render()
70+
{
71+
return view('livewire.mobile-pricing');
72+
}
73+
}

app/Livewire/PurchaseModal.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use Livewire\Attributes\Renderless;
6+
use Livewire\Attributes\Validate;
7+
use Livewire\Component;
8+
9+
class PurchaseModal extends Component
10+
{
11+
public bool $showModal = false;
12+
13+
#[Validate]
14+
public string $email = '';
15+
16+
public ?string $selectedPlan = null;
17+
18+
protected $rules = [
19+
'email' => 'required|email',
20+
];
21+
22+
#[Renderless]
23+
public function setPlan(string $plan): void
24+
{
25+
$this->selectedPlan = $plan;
26+
}
27+
28+
public function closeModal(): void
29+
{
30+
$this->showModal = false;
31+
$this->reset('email', 'selectedPlan');
32+
$this->resetValidation();
33+
}
34+
35+
public function submit(): void
36+
{
37+
$this->validate();
38+
39+
$this->dispatch('purchase-request-submitted', [
40+
'email' => $this->email,
41+
'plan' => $this->selectedPlan,
42+
]);
43+
44+
$this->closeModal();
45+
}
46+
47+
public function render()
48+
{
49+
return view('livewire.purchase-modal');
50+
}
51+
}

resources/views/early-adopter.blade.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ class="absolute inset-0 -z-10 h-full w-full object-cover"
608608
</section>
609609

610610
{{-- Pricing Section --}}
611-
<x-mobile-pricing />
611+
<livewire:mobile-pricing />
612612

613613
{{-- Testimonials Section --}}
614614
{{-- <x-testimonials /> --}}
@@ -676,11 +676,14 @@ class="mx-auto flex w-full max-w-2xl flex-col items-center gap-4 pt-10"
676676
</p>
677677
</x-faq-card>
678678

679-
<x-faq-card question="Will there ever be a free & open source version?">
679+
<x-faq-card
680+
question="Will there ever be a free & open source version?"
681+
>
680682
<p>
681-
Yes! Once we've hit sustainability and can afford to continue
682-
investing in this project indirectly, then a version of it will
683-
be fully open source and made available for free.
683+
Yes! Once we've hit sustainability and can afford to
684+
continue investing in this project indirectly, then a
685+
version of it will be fully open source and made available
686+
for free.
684687
</p>
685688
</x-faq-card>
686689

@@ -694,9 +697,7 @@ class="mx-auto flex w-full max-w-2xl flex-col items-center gap-4 pt-10"
694697
</x-faq-card>
695698

696699
<x-faq-card question="When will Android support be ready?">
697-
<p>
698-
It's READY! Sign up and build apps for Android today!
699-
</p>
700+
<p>It's READY! Sign up and build apps for Android today!</p>
700701
</x-faq-card>
701702

702703
<x-faq-card question="When will the EAP end?">
@@ -744,14 +745,15 @@ class="mx-auto flex w-full max-w-2xl flex-col items-center gap-4 pt-10"
744745
</x-faq-card>
745746
<x-faq-card question="Can I get an invoice?">
746747
<p>
747-
If you purchased after May 6, 2025, you should get an invoice with your receipt via email.
748+
If you purchased after May 6, 2025, you should get an
749+
invoice with your receipt via email.
748750
</p>
749751
<p class="mt-4">
750752
For purchases made before this, you simply need to
751753
<a
752754
href="https://zenvoice.io/p/67a61665e7a3400c73fb75af"
753755
onclick="event.stopPropagation()"
754-
class="underline inline-block"
756+
class="inline-block underline"
755757
>
756758
follow the instructions here
757759
</a>

resources/views/components/mobile-pricing.blade.php renamed to resources/views/livewire/mobile-pricing.blade.php

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,25 @@ class="size-5 shrink-0"
118118
</p>
119119
</div>
120120

121-
{{-- Button --}}
122-
<a
123-
href="{{ \App\Enums\Subscription::Mini->stripePaymentLink() }}"
124-
class="my-5 block w-full rounded-2xl bg-zinc-200 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
125-
aria-label="Get started with Mini plan"
126-
>
127-
Get started
128-
</a>
121+
@auth
122+
<button
123+
type="button"
124+
wire:click="createCheckoutSession('mini')"
125+
class="my-5 block w-full rounded-2xl bg-zinc-200 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
126+
aria-label="Get started with Mini plan"
127+
>
128+
Get started
129+
</button>
130+
@else
131+
<button
132+
type="button"
133+
@click="$dispatch('open-purchase-modal', { plan: 'mini' })"
134+
class="my-5 block w-full rounded-2xl bg-zinc-200 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
135+
aria-label="Get started with Mini plan"
136+
>
137+
Get started
138+
</button>
139+
@endauth
129140

130141
{{-- Features --}}
131142
<div
@@ -281,14 +292,25 @@ class="size-5 shrink-0"
281292
</p>
282293
</div>
283294

284-
{{-- Button --}}
285-
<a
286-
href="{{ \App\Enums\Subscription::Pro->stripePaymentLink() }}"
287-
class="my-5 block w-full rounded-2xl bg-zinc-200 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
288-
aria-label="Get started with Pro plan"
289-
>
290-
Get started
291-
</a>
295+
@auth
296+
<button
297+
type="button"
298+
wire:click="createCheckoutSession('pro')"
299+
class="my-5 block w-full rounded-2xl bg-zinc-200 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
300+
aria-label="Get started with Pro plan"
301+
>
302+
Get started
303+
</button>
304+
@else
305+
<button
306+
type="button"
307+
@click="$dispatch('open-purchase-modal', { plan: 'pro' })"
308+
class="my-5 block w-full rounded-2xl bg-zinc-200 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
309+
aria-label="Get started with Pro plan"
310+
>
311+
Get started
312+
</button>
313+
@endauth
292314

293315
{{-- Features --}}
294316
<div
@@ -452,14 +474,25 @@ class="size-5 shrink-0"
452474
</p>
453475
</div>
454476

455-
{{-- Button --}}
456-
<a
457-
href="{{ \App\Enums\Subscription::Max->stripePaymentLink() }}"
458-
class="my-5 block w-full rounded-2xl bg-zinc-800 py-4 text-center text-sm font-medium text-white transition duration-200 ease-in-out hover:bg-zinc-900 dark:bg-[#d68ffe] dark:text-black dark:hover:bg-[#e1acff]"
459-
aria-label="Get started with Max plan"
460-
>
461-
Get started
462-
</a>
477+
@auth
478+
<button
479+
type="button"
480+
wire:click="createCheckoutSession('max')"
481+
class="my-5 block w-full rounded-2xl bg-zinc-800 py-4 text-center text-sm font-medium text-white transition duration-200 ease-in-out hover:bg-zinc-900 dark:bg-[#d68ffe] dark:text-black dark:hover:bg-[#e1acff]"
482+
aria-label="Get started with Max plan"
483+
>
484+
Get started
485+
</button>
486+
@else
487+
<button
488+
type="button"
489+
@click="$dispatch('open-purchase-modal', { plan: 'max' })"
490+
class="my-5 block w-full rounded-2xl bg-zinc-800 py-4 text-center text-sm font-medium text-white transition duration-200 ease-in-out hover:bg-zinc-900 dark:bg-[#d68ffe] dark:text-black dark:hover:bg-[#e1acff]"
491+
aria-label="Get started with Max plan"
492+
>
493+
Get started
494+
</button>
495+
@endauth
463496

464497
{{-- Features --}}
465498
<div
@@ -569,4 +602,7 @@ class="grid size-7 shrink-0 place-items-center rounded-xl bg-[#D4FD7D] dark:bg-[
569602
</div>
570603
</div>
571604
</div>
605+
@guest
606+
<livewire:purchase-modal />
607+
@endguest
572608
</section>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<div>
2+
<div
3+
x-data="{
4+
open: @entangle('showModal'),
5+
}"
6+
@open-purchase-modal.window="
7+
open = true;
8+
$wire.setPlan($event.detail.plan);
9+
"
10+
>
11+
<!-- Modal Backdrop -->
12+
<div
13+
x-show="open"
14+
x-transition:enter="transition duration-300 ease-out"
15+
x-transition:enter-start="opacity-0"
16+
x-transition:enter-end="opacity-100"
17+
x-transition:leave="transition duration-200 ease-in"
18+
x-transition:leave-start="opacity-100"
19+
x-transition:leave-end="opacity-0"
20+
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
21+
x-cloak
22+
></div>
23+
24+
<div
25+
x-show="open"
26+
x-transition:enter="transition duration-300 ease-out"
27+
x-transition:enter-start="scale-95 opacity-0"
28+
x-transition:enter-end="scale-100 opacity-100"
29+
x-transition:leave="transition duration-200 ease-in"
30+
x-transition:leave-start="scale-100 opacity-100"
31+
x-transition:leave-end="scale-95 opacity-0"
32+
class="fixed inset-0 z-50 flex items-center justify-center p-4"
33+
x-cloak
34+
>
35+
<div
36+
@click.away="open = false"
37+
class="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl dark:bg-mirage"
38+
>
39+
<div class="mb-6 text-center">
40+
<h3 class="text-xl font-semibold dark:text-white">
41+
Get Started with NativePHP
42+
</h3>
43+
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
44+
Enter your email to continue to checkout
45+
</p>
46+
</div>
47+
48+
<form
49+
wire:submit.prevent="submit"
50+
class="space-y-8"
51+
>
52+
<div>
53+
<label
54+
for="email"
55+
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
56+
>
57+
Email Address
58+
</label>
59+
<input
60+
type="email"
61+
id="email"
62+
wire:model.blur="email"
63+
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 focus:border-purple-400 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
64+
placeholder="your@email.com"
65+
required
66+
x-effect="if (open) $nextTick(() => $el.focus())"
67+
/>
68+
@error('email')
69+
<p class="mt-1 text-sm text-red-600">
70+
{{ $message }}
71+
</p>
72+
@enderror
73+
</div>
74+
75+
<div class="flex items-center justify-between gap-3">
76+
<button
77+
type="button"
78+
wire:click="closeModal"
79+
class="rounded-xl bg-zinc-200 px-8 py-4 text-center text-sm font-medium transition duration-200 ease-in-out hover:bg-zinc-800 hover:text-white dark:bg-slate-700/30 dark:hover:bg-slate-700/40"
80+
>
81+
Cancel
82+
</button>
83+
<button
84+
type="submit"
85+
class="rounded-xl bg-zinc-800 px-8 py-4 text-center text-sm font-medium text-white transition duration-200 ease-in-out hover:bg-zinc-900 dark:bg-[#d68ffe] dark:text-black dark:hover:bg-[#e1acff]"
86+
>
87+
Next
88+
</button>
89+
</div>
90+
</form>
91+
</div>
92+
</div>
93+
</div>
94+
</div>

0 commit comments

Comments
 (0)
0