Skip to content

Commit 5a1a1c0

Browse files
committed
Merge branch 'main' of github.com:NativePHP/nativephp.com into v2
2 parents eb67a4a + f1f2941 commit 5a1a1c0

File tree

86 files changed

+5000
-238
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5000
-238
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace App\Actions\SubLicenses;
4+
5+
use App\Models\SubLicense;
6+
use App\Services\Anystack\Anystack;
7+
8+
class DeleteSubLicense
9+
{
10+
/**
11+
* Handle the deletion of a sub-license.
12+
*/
13+
public function handle(SubLicense $subLicense): bool
14+
{
15+
Anystack::api()
16+
->license($subLicense->anystack_id, $subLicense->parentLicense->subscriptionType->anystackProductId())
17+
->delete();
18+
19+
return $subLicense->delete();
20+
}
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace App\Actions\SubLicenses;
4+
5+
use App\Models\SubLicense;
6+
use App\Services\Anystack\Anystack;
7+
8+
class SuspendSubLicense
9+
{
10+
/**
11+
* Handle the suspension of a sub-license.
12+
*/
13+
public function handle(SubLicense $subLicense): SubLicense
14+
{
15+
Anystack::api()
16+
->license($subLicense->anystack_id, $subLicense->parentLicense->subscriptionType->anystackProductId())
17+
->suspend();
18+
19+
$subLicense->update([
20+
'is_suspended' => true,
21+
]);
22+
23+
return $subLicense;
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace App\Actions\SubLicenses;
4+
5+
use App\Models\SubLicense;
6+
use App\Services\Anystack\Anystack;
7+
8+
class UnsuspendSubLicense
9+
{
10+
/**
11+
* Handle the un-suspension of a sub-license.
12+
*/
13+
public function handle(SubLicense $subLicense): SubLicense
14+
{
15+
Anystack::api()
16+
->license($subLicense->anystack_id, $subLicense->parentLicense->subscriptionType->anystackProductId())
17+
->suspend(false);
18+
19+
$subLicense->update([
20+
'is_suspended' => false,
21+
]);
22+
23+
return $subLicense;
24+
}
25+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Jobs\UpdateAnystackLicenseExpiryJob;
6+
use App\Models\License;
7+
use Illuminate\Console\Command;
8+
9+
class ExtendLicenseExpiryCommand extends Command
10+
{
11+
/**
12+
* The name and signature of the console command.
13+
*
14+
* @var string
15+
*/
16+
protected $signature = 'licenses:renew {license_id}';
17+
18+
/**
19+
* The console command description.
20+
*
21+
* @var string
22+
*/
23+
protected $description = 'Extend the expiry date of a license';
24+
25+
/**
26+
* Execute the console command.
27+
*/
28+
public function handle(): int
29+
{
30+
$licenseId = $this->argument('license_id');
31+
32+
// Find the license
33+
$license = License::find($licenseId);
34+
if (! $license) {
35+
$this->error("License with ID {$licenseId} not found");
36+
37+
return Command::FAILURE;
38+
}
39+
40+
// Dispatch the job to update the license expiry
41+
UpdateAnystackLicenseExpiryJob::dispatch($license);
42+
43+
$this->info("License expiry updated to {$license->expires_at->format('Y-m-d')}");
44+
45+
return Command::SUCCESS;
46+
}
47+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\License;
6+
use App\Notifications\LicenseExpiryWarning;
7+
use Illuminate\Console\Command;
8+
9+
class SendLicenseExpiryWarnings extends Command
10+
{
11+
protected $signature = 'licenses:send-expiry-warnings';
12+
13+
protected $description = 'Send expiry warning emails for licenses that are expiring soon';
14+
15+
public function handle(): int
16+
{
17+
$warningDays = [30, 7, 1];
18+
$totalSent = 0;
19+
20+
foreach ($warningDays as $days) {
21+
$sent = $this->sendWarningsForDays($days);
22+
$totalSent += $sent;
23+
24+
$this->info("Sent {$sent} warning emails for licenses expiring in {$days} day(s)");
25+
}
26+
27+
$this->info("Total warning emails sent: {$totalSent}");
28+
29+
return Command::SUCCESS;
30+
}
31+
32+
private function sendWarningsForDays(int $days): int
33+
{
34+
$targetDate = now()->addDays($days)->startOfDay();
35+
$sent = 0;
36+
37+
// Find licenses that:
38+
// 1. Expire on the target date
39+
// 2. Don't have an active subscription (legacy licenses)
40+
// 3. Haven't been sent a warning for this specific day count recently
41+
$licenses = License::query()
42+
->whereDate('expires_at', $targetDate)
43+
->whereNull('subscription_item_id') // Legacy licenses without subscriptions
44+
->whereDoesntHave('expiryWarnings', function ($query) use ($days) {
45+
$query->where('warning_days', $days)
46+
->where('sent_at', '>=', now()->subHours(23)); // Prevent duplicate emails within 23 hours
47+
})
48+
->with('user')
49+
->get();
50+
51+
foreach ($licenses as $license) {
52+
if ($license->user) {
53+
$license->user->notify(new LicenseExpiryWarning($license, $days));
54+
55+
// Track that we sent this warning
56+
$license->expiryWarnings()->create([
57+
'warning_days' => $days,
58+
'sent_at' => now(),
59+
]);
60+
61+
$sent++;
62+
63+
$this->line("Sent {$days}-day warning to {$license->user->email} for license {$license->key}");
64+
}
65+
}
66+
67+
return $sent;
68+
}
69+
}

app/Console/Kernel.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ class Kernel extends ConsoleKernel
1212
*/
1313
protected function schedule(Schedule $schedule): void
1414
{
15-
// $schedule->command('inspire')->hourly();
15+
// Send license expiry warnings daily at 9 AM UTC
16+
$schedule->command('licenses:send-expiry-warnings')
17+
->dailyAt('09:00')
18+
->onOneServer()
19+
->runInBackground();
1620
}
1721

1822
/**

app/Enums/Subscription.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ public function name(): string
5353
return config("subscriptions.plans.{$this->value}.name");
5454
}
5555

56-
public function stripePriceId(): string
56+
public function stripePriceId(bool $forceEap = false): string
5757
{
5858
// EAP ends June 1st at midnight UTC
59-
return now()->isBefore('2025-06-01 00:00:00')
59+
return now()->isBefore('2025-06-01 00:00:00') || $forceEap
6060
? config("subscriptions.plans.{$this->value}.stripe_price_id_eap")
6161
: config("subscriptions.plans.{$this->value}.stripe_price_id");
6262
}
@@ -75,4 +75,18 @@ public function anystackPolicyId(): string
7575
{
7676
return config("subscriptions.plans.{$this->value}.anystack_policy_id");
7777
}
78+
79+
public function supportsSubLicenses(): bool
80+
{
81+
return in_array($this, [self::Pro, self::Max, self::Forever]);
82+
}
83+
84+
public function subLicenseLimit(): ?int
85+
{
86+
return match ($this) {
87+
self::Pro => 9,
88+
self::Max, self::Forever => null, // Unlimited
89+
default => 0,
90+
};
91+
}
7892
}

app/Features/ShowAuthButtons.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Features;
4+
5+
use Laravel\Pennant\Feature;
6+
7+
class ShowAuthButtons
8+
{
9+
/**
10+
* Resolve the feature's initial value.
11+
*/
12+
public function resolve(mixed $scope): bool
13+
{
14+
if ($scope) {
15+
return Feature::for(null)->active(static::class);
16+
}
17+
18+
return false;
19+
}
20+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Auth;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Auth\LoginRequest;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Support\Facades\Auth;
10+
use Illuminate\View\View;
11+
12+
class CustomerAuthController extends Controller
13+
{
14+
public function showLogin(): View
15+
{
16+
return view('auth.login');
17+
}
18+
19+
public function login(LoginRequest $request): RedirectResponse
20+
{
21+
$request->authenticate();
22+
23+
$request->session()->regenerate();
24+
25+
return redirect()->intended(route('customer.licenses'));
26+
}
27+
28+
public function logout(Request $request): RedirectResponse
29+
{
30+
Auth::logout();
31+
32+
$request->session()->invalidate();
33+
$request->session()->regenerateToken();
34+
35+
return redirect()->route('customer.login');
36+
}
37+
38+
public function showForgotPassword(): View
39+
{
40+
return view('auth.forgot-password');
41+
}
42+
43+
public function sendPasswordResetLink(Request $request): RedirectResponse
44+
{
45+
$request->validate([
46+
'email' => ['required', 'email:rfc,dns'],
47+
]);
48+
49+
$status = \Illuminate\Support\Facades\Password::sendResetLink(
50+
$request->only('email')
51+
);
52+
53+
return $status === \Illuminate\Auth\Passwords\PasswordBroker::RESET_LINK_SENT
54+
? back()->with(['status' => __($status)])
55+
: back()->withErrors(['email' => __($status)]);
56+
}
57+
58+
public function showResetPassword(string $token): View
59+
{
60+
return view('auth.reset-password', ['token' => $token]);
61+
}
62+
63+
public function resetPassword(Request $request): RedirectResponse
64+
{
65+
$request->validate([
66+
'token' => ['required'],
67+
'email' => ['required', 'email:rfc,dns'],
68+
'password' => ['required', 'min:8', 'confirmed'],
69+
]);
70+
71+
$status = \Illuminate\Support\Facades\Password::reset(
72+
$request->only('email', 'password', 'password_confirmation', 'token'),
73+
function ($user, $password) {
74+
$user->forceFill([
75+
'password' => $password,
76+
]);
77+
78+
$user->save();
79+
}
80+
);
81+
82+
return $status === \Illuminate\Auth\Passwords\PasswordBroker::PASSWORD_RESET
83+
? redirect()->route('customer.login')->with('status', __($status))
84+
: back()->withErrors(['email' => [__($status)]]);
85+
}
86+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\RedirectResponse;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Auth;
8+
use Illuminate\View\View;
9+
10+
class CustomerLicenseController extends Controller
11+
{
12+
public function __construct()
13+
{
14+
$this->middleware('auth');
15+
}
16+
17+
public function index(): View
18+
{
19+
$user = Auth::user();
20+
$licenses = $user->licenses()->orderBy('created_at', 'desc')->get();
21+
22+
return view('customer.licenses.index', compact('licenses'));
23+
}
24+
25+
public function show(string $licenseKey): View
26+
{
27+
$user = Auth::user();
28+
$license = $user->licenses()
29+
->with('subLicenses')
30+
->where('key', $licenseKey)
31+
->firstOrFail();
32+
33+
return view('customer.licenses.show', compact('license'));
34+
}
35+
36+
public function update(Request $request, string $licenseKey): RedirectResponse
37+
{
38+
$user = Auth::user();
39+
$license = $user->licenses()->where('key', $licenseKey)->firstOrFail();
40+
41+
$request->validate([
42+
'name' => ['nullable', 'string', 'max:255'],
43+
]);
44+
45+
$license->update([
46+
'name' => $request->name,
47+
]);
48+
49+
return redirect()->route('customer.licenses.show', $licenseKey)
50+
->with('success', 'License name updated successfully!');
51+
}
52+
}

0 commit comments

Comments
 (0)