Skip to content

Commit 09c1a39

Browse files
committed
Merge branch 'main' into hassan/asg-37-design-a-wall-of-love-page-for-eap-members-pre-eap-and-eap
2 parents 95860dd + 0f197cd commit 09c1a39

27 files changed

+953
-258
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\User;
6+
use Illuminate\Console\Command;
7+
use Stripe\Customer;
8+
use Stripe\Exception\ApiErrorException;
9+
10+
class MatchUsersWithStripeCustomers extends Command
11+
{
12+
protected $signature = 'users:match-stripe-customers
13+
{--limit= : Limit the number of users to process}
14+
{--dry-run : Show what would be updated without making changes}';
15+
16+
protected $description = 'Match users without Stripe IDs to their Stripe customer records';
17+
18+
public function handle(): int
19+
{
20+
$query = User::whereNull('stripe_id');
21+
22+
$totalUsers = $query->count();
23+
24+
if ($totalUsers === 0) {
25+
$this->info('No users found without Stripe IDs.');
26+
27+
return self::SUCCESS;
28+
}
29+
30+
$this->info("Found {$totalUsers} users without Stripe IDs.");
31+
32+
$limit = $this->option('limit');
33+
if ($limit) {
34+
$query->limit((int) $limit);
35+
$this->info("Processing first {$limit} users...");
36+
}
37+
38+
$dryRun = $this->option('dry-run');
39+
if ($dryRun) {
40+
$this->warn('DRY RUN MODE - No changes will be made');
41+
}
42+
43+
$users = $query->get();
44+
45+
$matched = 0;
46+
$notFound = 0;
47+
$errors = 0;
48+
49+
$progressBar = $this->output->createProgressBar($users->count());
50+
$progressBar->start();
51+
52+
/** @var User $user */
53+
foreach ($users as $user) {
54+
try {
55+
/** @var Customer $customer */
56+
$customer = $user->findStripeCustomerRecords()->first(fn (Customer $result) => $result->next_invoice_sequence === 1);
57+
58+
if ($customer) {
59+
$matched++;
60+
61+
if (! $dryRun) {
62+
$user->update(['stripe_id' => $customer->id]);
63+
}
64+
65+
$this->newLine();
66+
$this->line(" ✓ Matched: {$user->email}{$customer->id}");
67+
} else {
68+
$notFound++;
69+
$this->newLine();
70+
$this->line(" - No match: {$user->email}");
71+
}
72+
} catch (ApiErrorException $e) {
73+
$errors++;
74+
$this->newLine();
75+
$this->error(" ✗ Error for {$user->email}: {$e->getMessage()}");
76+
} catch (\Exception $e) {
77+
$errors++;
78+
$this->newLine();
79+
$this->error(" ✗ Unexpected error for {$user->email}: {$e->getMessage()}");
80+
}
81+
82+
$progressBar->advance();
83+
84+
// Add a small delay to avoid rate limiting
85+
usleep(100000); // 0.1 seconds
86+
}
87+
88+
$progressBar->finish();
89+
$this->newLine(2);
90+
91+
// Summary
92+
$this->info('Summary:');
93+
$this->table(
94+
['Status', 'Count'],
95+
[
96+
['Matched', $matched],
97+
['Not Found', $notFound],
98+
['Errors', $errors],
99+
['Total Processed', $users->count()],
100+
]
101+
);
102+
103+
if ($dryRun) {
104+
$this->warn('This was a dry run. Run without --dry-run to apply changes.');
105+
}
106+
107+
return self::SUCCESS;
108+
}
109+
}

app/Http/Controllers/ShowBlogController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function show(Article $article)
2323
abort_unless($article->isPublished() || auth()->user()?->isAdmin(), 404);
2424

2525
// Set SEO metadata for the article
26-
SEOTools::setTitle($article->title . ' - Blog');
26+
SEOTools::setTitle($article->title.' - Blog');
2727
SEOTools::setDescription($article->excerpt ?: 'Read this article on the NativePHP blog.');
2828

2929
// Set OpenGraph metadata

app/Http/Controllers/ShowDocumentationController.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ public function __invoke(Request $request, string $platform, string $version, ?s
4848
}
4949
$title = $pageProperties['title'].' - NativePHP '.$platform.' v'.$version;
5050
$description = Arr::exists($pageProperties, 'description') ? $pageProperties['description'] : 'NativePHP documentation for '.$platform.' v'.$version;
51-
51+
5252
SEOTools::setTitle($title);
5353
SEOTools::setDescription($description);
54-
54+
5555
// Set OpenGraph metadata
5656
SEOTools::opengraph()->setTitle($pageProperties['title']);
5757
SEOTools::opengraph()->setDescription($description);
5858
SEOTools::opengraph()->setType('article');
59-
59+
6060
// Set Twitter Card metadata
6161
SEOTools::twitter()->setTitle($pageProperties['title']);
6262
SEOTools::twitter()->setDescription($description);

app/Livewire/SubLicenseManager.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use App\Models\License;
6+
use Livewire\Component;
7+
8+
class SubLicenseManager extends Component
9+
{
10+
public License $license;
11+
12+
public bool $isPolling = false;
13+
14+
public int $initialSubLicenseCount;
15+
16+
public function mount(License $license): void
17+
{
18+
$this->license = $license;
19+
$this->initialSubLicenseCount = $license->subLicenses->count();
20+
}
21+
22+
public function startPolling(): void
23+
{
24+
$this->isPolling = true;
25+
}
26+
27+
public function render()
28+
{
29+
// Refresh the license and sublicenses from the database
30+
$this->license->refresh();
31+
$this->license->load('subLicenses');
32+
33+
// Check if a new sublicense has been added
34+
if ($this->isPolling && $this->license->subLicenses->count() > $this->initialSubLicenseCount) {
35+
$this->isPolling = false;
36+
$this->initialSubLicenseCount = $this->license->subLicenses->count();
37+
}
38+
39+
$activeSubLicenses = $this->license->subLicenses->where('is_suspended', false);
40+
$suspendedSubLicenses = $this->license->subLicenses->where('is_suspended', true);
41+
42+
return view('livewire.sub-license-manager', [
43+
'activeSubLicenses' => $activeSubLicenses,
44+
'suspendedSubLicenses' => $suspendedSubLicenses,
45+
]);
46+
}
47+
}

app/Models/License.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Database\Eloquent\Model;
1010
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1111
use Illuminate\Database\Eloquent\Relations\HasMany;
12+
use Illuminate\Support\Carbon;
1213
use Laravel\Cashier\SubscriptionItem;
1314

1415
class License extends Model
@@ -115,6 +116,12 @@ public function canCreateSubLicense(): bool
115116
return $remaining === null || $remaining > 0;
116117
}
117118

119+
public function isLegacy(): bool
120+
{
121+
return !$this->subscription_item_id
122+
&& $this->created_at->lt(Carbon::create(2025, 5, 8));
123+
}
124+
118125
public function suspendAllSubLicenses(): int
119126
{
120127
return $this->subLicenses()->update(['is_suspended' => true]);

app/Models/User.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
use Illuminate\Database\Eloquent\Relations\HasMany;
1010
use Illuminate\Foundation\Auth\User as Authenticatable;
1111
use Illuminate\Notifications\Notifiable;
12+
use Illuminate\Support\Collection;
1213
use Laravel\Cashier\Billable;
1314
use Laravel\Sanctum\HasApiTokens;
15+
use Stripe\Customer;
1416

1517
class User extends Authenticatable implements FilamentUser
1618
{
@@ -75,4 +77,13 @@ public function getLastNameAttribute(): ?string
7577

7678
return $nameParts[1] ?? null;
7779
}
80+
81+
public function findStripeCustomerRecords(): Collection
82+
{
83+
$search = static::stripe()->customers->search([
84+
'query' => 'email:"'.$this->email.'"',
85+
]);
86+
87+
return collect($search->data);
88+
}
7889
}

app/Notifications/LicenseExpiryWarning.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private function getMainMessage(): string
5959
return match ($this->daysUntilExpiry) {
6060
30 => 'This is a friendly reminder that your NativePHP license will expire in 30 days.',
6161
7 => 'Your NativePHP license will expire in just 7 days. This is an important reminder to set up your renewal.',
62-
1 => 'Your NativePHP license expires tomorrow! Please take immediate action to avoid service interruption.',
62+
1 => 'Your NativePHP license expires tomorrow! Please take immediate action to avoid interruption.',
6363
0 => 'Your NativePHP license expires today. Renew now to maintain access.',
6464
default => "Your NativePHP license will expire in {$this->daysUntilExpiry} days.",
6565
};

resources/css/app.css

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,16 @@ nav.docs-navigation > ul > li > ul {
194194
}
195195

196196
.prose pre code {
197-
@apply text-gray-50;
197+
@apply bg-transparent p-0 text-sm font-normal text-gray-50;
198198
}
199199

200200
.prose code {
201-
@apply px-1;
201+
@apply rounded bg-gray-200 px-1.5 py-0.5 text-sm font-medium text-purple-600;
202+
}
203+
204+
.prose code::before,
205+
.prose code::after {
206+
content: none;
202207
}
203208

204209
.prose a {
@@ -263,5 +268,5 @@ nav.docs-navigation > ul > li > ul {
263268
}
264269

265270
.dark .prose code {
266-
@apply text-gray-300;
271+
@apply bg-gray-800 text-purple-300;
267272
}

resources/css/docsearch.css

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
}
1212

1313
.DocSearch-Button {
14-
@apply m-0 flex items-center rounded-full bg-gray-50/50 font-normal ring-1 ring-slate-600/30 transition duration-300 ease-out dark:bg-black/30;
14+
@apply m-0 flex items-center rounded-full bg-gray-50/50 font-normal ring-1 ring-slate-600/30 transition duration-300 ease-out ring-inset dark:bg-black/30;
1515
}
1616

1717
.DocSearch-Button:hover {
@@ -28,15 +28,8 @@
2828
@apply text-gray-500 dark:text-white/60;
2929
}
3030

31-
@media (max-width: 768px) {
32-
.DocSearch-Button-Keys,
33-
.DocSearch-Button-Placeholder {
34-
display: block;
35-
}
36-
}
37-
3831
.DocSearch-Button-Placeholder {
39-
@apply px-1 text-sm text-black/60 transition duration-300 min-[520px]:pr-10 dark:text-white/60;
32+
@apply px-1 text-sm text-black/60 transition duration-300 xl:pr-5 dark:text-white/60;
4033
}
4134

4235
.DocSearch-Button-Keys {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
@props([
2+
'small' => false,
3+
])
4+
5+
<a
6+
href="https://bifrost.nativephp.com/"
7+
@class([
8+
'group relative z-0 inline-flex items-center overflow-hidden rounded-full bg-gray-200 transition duration-200 will-change-transform hover:scale-x-105 dark:bg-slate-800',
9+
10+
'px-4 py-2 text-sm' => $small,
11+
12+
'px-6 py-3' => ! $small,
13+
])
14+
>
15+
<div
16+
class="@container absolute inset-0 flex items-center"
17+
aria-hidden="true"
18+
>
19+
<div
20+
class="absolute h-[100cqw] w-[100cqw] scale-110 animate-[spin_3s_linear_infinite] bg-[conic-gradient(from_0_at_50%_50%,--alpha(var(--color-blue-400)/70%)_0deg,--alpha(var(--color-indigo-400)/70%)_40deg,--alpha(var(--color-orange-300)/70%)_80deg,transparent_110deg,transparent_250deg,--alpha(var(--color-rose-400)/50%)_280deg,--alpha(var(--color-fuchsia-400)/50%)_320deg,--alpha(var(--color-blue-400)/70%)_360deg)] transition duration-300 will-change-transform dark:bg-[conic-gradient(from_0_at_50%_50%,--alpha(var(--color-blue-400)/70%)_0deg,--alpha(var(--color-indigo-400)/70%)_40deg,--alpha(var(--color-teal-400)/70%)_80deg,transparent_110deg,transparent_250deg,--alpha(var(--color-rose-400)/50%)_280deg,--alpha(var(--color-fuchsia-400)/50%)_320deg,--alpha(var(--color-blue-400)/70%)_360deg)]"
21+
></div>
22+
</div>
23+
<div
24+
class="absolute inset-0.5 rounded-full bg-white dark:bg-slate-950"
25+
aria-hidden="true"
26+
></div>
27+
<div
28+
class="absolute bottom-0 left-1/2 h-1/3 w-4/5 -translate-x-1/2 rounded-full bg-indigo-400/15 opacity-50 blur-md transition-all duration-500 group-hover:h-2/3 group-hover:opacity-100"
29+
aria-hidden="true"
30+
></div>
31+
<span
32+
x-data
33+
x-init="
34+
(() => {
35+
const items = Array.from($el.children)
36+
if (items.length === 0) {
37+
return
38+
}
39+
40+
// Initial state: show first, hide others (coming from below)
41+
gsap.set(items, { autoAlpha: 0, y: 10 })
42+
gsap.set(items[0], { autoAlpha: 1, y: 0 })
43+
44+
if (items.length === 1) {
45+
return
46+
}
47+
48+
const hold = 0.7 // seconds each word stays visible
49+
const tl = gsap.timeline({ repeat: -1 })
50+
51+
for (let i = 0; i < items.length; i++) {
52+
const curr = items[i]
53+
const next = items[(i + 1) % items.length]
54+
55+
tl.to(
56+
curr,
57+
{
58+
duration: 0.5,
59+
autoAlpha: 0,
60+
y: -10,
61+
ease: 'circ.inOut',
62+
},
63+
`+=${hold}`,
64+
).to(
65+
next,
66+
{
67+
duration: 0.5,
68+
autoAlpha: 1,
69+
y: 0,
70+
ease: 'circ.inOut',
71+
},
72+
'<',
73+
)
74+
}
75+
})()
76+
"
77+
class="inline-grid font-medium"
78+
>
79+
<span
80+
class="self-center justify-self-center whitespace-nowrap [grid-area:1/-1]"
81+
>
82+
Try Bifrost!
83+
</span>
84+
<span
85+
class="self-center justify-self-center whitespace-nowrap [grid-area:1/-1]"
86+
>
87+
Build
88+
</span>
89+
<span
90+
class="self-center justify-self-center whitespace-nowrap [grid-area:1/-1]"
91+
>
92+
Distribute
93+
</span>
94+
<span
95+
class="self-center justify-self-center whitespace-nowrap [grid-area:1/-1]"
96+
>
97+
{{ $small ? 'Ship' : 'Ship it!' }}
98+
</span>
99+
</span>
100+
</a>

0 commit comments

Comments
 (0)