Skip to content

Commit 9c9da7f

Browse files
authored
Merge pull request #384 from NativePHP/discord-early-adopter-role
Add Discord Early Adopter role for EAP customers
2 parents 9b78de5 + 7e8896f commit 9c9da7f

10 files changed

Lines changed: 627 additions & 47 deletions

File tree

app/Http/Controllers/DiscordIntegrationController.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,30 @@ public function handleCallback(): RedirectResponse
7070

7171
if (! $discord->isGuildMember($discordUser['id'])) {
7272
return to_route('customer.integrations')
73-
->with('warning', 'Discord account connected! Please join the NativePHP Discord server to receive the Max role.');
73+
->with('warning', 'Discord account connected! Please join the NativePHP Discord server to receive your roles.');
7474
}
7575

76-
if ($user->hasMaxAccess()) {
77-
$success = $discord->assignMaxRole($discordUser['id']);
76+
$rolesAssigned = [];
7877

79-
if ($success) {
80-
$user->update([
81-
'discord_role_granted_at' => now(),
82-
]);
78+
if ($user->hasMaxAccess()) {
79+
if ($discord->assignMaxRole($discordUser['id'])) {
80+
$user->update(['discord_role_granted_at' => now()]);
81+
$rolesAssigned[] = 'Max';
82+
}
83+
}
8384

84-
return to_route('customer.integrations')
85-
->with('success', 'Discord account connected and Max role assigned!');
85+
if ($user->isEapCustomer()) {
86+
if ($discord->assignEarlyAdopterRole($discordUser['id'])) {
87+
$user->update(['discord_early_adopter_role_granted_at' => now()]);
88+
$rolesAssigned[] = 'Early Adopter';
8689
}
90+
}
91+
92+
if (count($rolesAssigned) > 0) {
93+
$roleNames = implode(' and ', $rolesAssigned);
8794

8895
return to_route('customer.integrations')
89-
->with('warning', 'Discord account connected, but we could not assign the Max role. Please try again later.');
96+
->with('success', "Discord account connected and {$roleNames} role(s) assigned!");
9097
}
9198

9299
return to_route('customer.integrations')
@@ -106,15 +113,23 @@ public function disconnect(): RedirectResponse
106113
{
107114
$user = Auth::user();
108115

109-
if ($user->discord_role_granted_at && $user->discord_id) {
116+
if ($user->discord_id) {
110117
$discord = DiscordApi::make();
111-
$discord->removeMaxRole($user->discord_id);
118+
119+
if ($user->discord_role_granted_at) {
120+
$discord->removeMaxRole($user->discord_id);
121+
}
122+
123+
if ($user->discord_early_adopter_role_granted_at) {
124+
$discord->removeEarlyAdopterRole($user->discord_id);
125+
}
112126
}
113127

114128
$user->update([
115129
'discord_id' => null,
116130
'discord_username' => null,
117131
'discord_role_granted_at' => null,
132+
'discord_early_adopter_role_granted_at' => null,
118133
]);
119134

120135
return back()->with('success', 'Discord account disconnected successfully.');

app/Livewire/DiscordAccessBanner.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class DiscordAccessBanner extends Component
1212

1313
public bool $hasMaxRole = false;
1414

15+
public bool $hasEarlyAdopterRole = false;
16+
1517
public bool $isGuildMember = false;
1618

1719
public function mount(bool $inline = false): void
@@ -26,6 +28,7 @@ public function checkRoleStatus(): void
2628

2729
if (! $user || ! $user->discord_id) {
2830
$this->hasMaxRole = false;
31+
$this->hasEarlyAdopterRole = false;
2932
$this->isGuildMember = false;
3033

3134
return;
@@ -39,15 +42,21 @@ public function checkRoleStatus(): void
3942
return [
4043
'isGuildMember' => $discord->isGuildMember($user->discord_id),
4144
'hasMaxRole' => $discord->hasMaxRole($user->discord_id),
45+
'hasEarlyAdopterRole' => $discord->hasEarlyAdopterRole($user->discord_id),
4246
];
4347
});
4448

4549
$this->isGuildMember = $status['isGuildMember'];
4650
$this->hasMaxRole = $status['hasMaxRole'];
51+
$this->hasEarlyAdopterRole = $status['hasEarlyAdopterRole'];
4752

4853
if ($this->hasMaxRole && ! $user->discord_role_granted_at) {
4954
$user->update(['discord_role_granted_at' => now()]);
5055
}
56+
57+
if ($this->hasEarlyAdopterRole && ! $user->discord_early_adopter_role_granted_at) {
58+
$user->update(['discord_early_adopter_role_granted_at' => now()]);
59+
}
5160
}
5261

5362
public function refreshStatus(): void
@@ -97,6 +106,42 @@ public function requestMaxRole(): void
97106
}
98107
}
99108

109+
public function requestEarlyAdopterRole(): void
110+
{
111+
$user = auth()->user();
112+
113+
if (! $user || ! $user->discord_id) {
114+
session()->flash('error', 'Please connect your Discord account first.');
115+
116+
return;
117+
}
118+
119+
if (! $user->isEapCustomer()) {
120+
session()->flash('error', 'The Early Adopter role is for early access program customers.');
121+
122+
return;
123+
}
124+
125+
$discord = DiscordApi::make();
126+
127+
if (! $discord->isGuildMember($user->discord_id)) {
128+
session()->flash('error', 'Please join the NativePHP Discord server first.');
129+
130+
return;
131+
}
132+
133+
$success = $discord->assignEarlyAdopterRole($user->discord_id);
134+
135+
if ($success) {
136+
$user->update(['discord_early_adopter_role_granted_at' => now()]);
137+
Cache::forget("discord_role_status_{$user->id}");
138+
$this->checkRoleStatus();
139+
session()->flash('success', 'Early Adopter role assigned successfully!');
140+
} else {
141+
session()->flash('error', 'Failed to assign Early Adopter role. Please try again later.');
142+
}
143+
}
144+
100145
public function render()
101146
{
102147
return view('livewire.discord-access-banner');

app/Models/User.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ protected function casts(): array
570570
'mobile_repo_access_granted_at' => 'datetime',
571571
'claude_plugins_repo_access_granted_at' => 'datetime',
572572
'discord_role_granted_at' => 'datetime',
573+
'discord_early_adopter_role_granted_at' => 'datetime',
573574
];
574575
}
575576
}

app/Support/DiscordApi.php

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ class DiscordApi
1212
public function __construct(
1313
private ?string $botToken,
1414
private ?string $guildId,
15-
private ?string $maxRoleId
15+
private ?string $maxRoleId,
16+
private ?string $earlyAdopterRoleId
1617
) {}
1718

1819
public static function make(): static
1920
{
2021
return new static(
2122
config('services.discord.bot_token', ''),
2223
config('services.discord.guild_id', ''),
23-
config('services.discord.max_role_id', '')
24+
config('services.discord.max_role_id', ''),
25+
config('services.discord.early_adopter_role_id', '')
2426
);
2527
}
2628

@@ -70,18 +72,48 @@ public function isGuildMember(string $discordUserId): bool
7072
}
7173

7274
public function assignMaxRole(string $discordUserId): bool
75+
{
76+
return $this->assignRole($discordUserId, $this->maxRoleId, 'Max');
77+
}
78+
79+
public function removeMaxRole(string $discordUserId): bool
80+
{
81+
return $this->removeRole($discordUserId, $this->maxRoleId, 'Max');
82+
}
83+
84+
public function hasMaxRole(string $discordUserId): bool
85+
{
86+
return $this->hasRole($discordUserId, $this->maxRoleId);
87+
}
88+
89+
public function assignEarlyAdopterRole(string $discordUserId): bool
90+
{
91+
return $this->assignRole($discordUserId, $this->earlyAdopterRoleId, 'Early Adopter');
92+
}
93+
94+
public function removeEarlyAdopterRole(string $discordUserId): bool
95+
{
96+
return $this->removeRole($discordUserId, $this->earlyAdopterRoleId, 'Early Adopter');
97+
}
98+
99+
public function hasEarlyAdopterRole(string $discordUserId): bool
100+
{
101+
return $this->hasRole($discordUserId, $this->earlyAdopterRoleId);
102+
}
103+
104+
private function assignRole(string $discordUserId, ?string $roleId, string $roleName): bool
73105
{
74106
$response = Http::withToken($this->botToken, 'Bot')
75107
->put(sprintf(
76108
'%s/guilds/%s/members/%s/roles/%s',
77109
self::BASE_URL,
78110
$this->guildId,
79111
$discordUserId,
80-
$this->maxRoleId
112+
$roleId
81113
));
82114

83115
if ($response->failed()) {
84-
Log::error('Failed to assign Discord Max role', [
116+
Log::error("Failed to assign Discord {$roleName} role", [
85117
'discord_user_id' => $discordUserId,
86118
'status' => $response->status(),
87119
'response' => $response->json(),
@@ -93,19 +125,19 @@ public function assignMaxRole(string $discordUserId): bool
93125
return true;
94126
}
95127

96-
public function removeMaxRole(string $discordUserId): bool
128+
private function removeRole(string $discordUserId, ?string $roleId, string $roleName): bool
97129
{
98130
$response = Http::withToken($this->botToken, 'Bot')
99131
->delete(sprintf(
100132
'%s/guilds/%s/members/%s/roles/%s',
101133
self::BASE_URL,
102134
$this->guildId,
103135
$discordUserId,
104-
$this->maxRoleId
136+
$roleId
105137
));
106138

107139
if ($response->failed()) {
108-
Log::error('Failed to remove Discord Max role', [
140+
Log::error("Failed to remove Discord {$roleName} role", [
109141
'discord_user_id' => $discordUserId,
110142
'status' => $response->status(),
111143
'response' => $response->json(),
@@ -117,7 +149,7 @@ public function removeMaxRole(string $discordUserId): bool
117149
return true;
118150
}
119151

120-
public function hasMaxRole(string $discordUserId): bool
152+
private function hasRole(string $discordUserId, ?string $roleId): bool
121153
{
122154
$response = Http::withToken($this->botToken, 'Bot')
123155
->get(sprintf(
@@ -140,6 +172,6 @@ public function hasMaxRole(string $discordUserId): bool
140172
$member = $response->json();
141173
$roles = $member['roles'] ?? [];
142174

143-
return in_array($this->maxRoleId, $roles, true);
175+
return in_array($roleId, $roles, true);
144176
}
145177
}

config/services.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
'bot_token' => env('DISCORD_BOT_TOKEN'),
5858
'guild_id' => env('DISCORD_GUILD_ID'),
5959
'max_role_id' => env('DISCORD_MAX_ROLE_ID'),
60+
'early_adopter_role_id' => env('DISCORD_EARLY_ADOPTER_ROLE_ID'),
6061
],
6162

6263
'turnstile' => [
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('users', function (Blueprint $table) {
15+
$table->timestamp('discord_early_adopter_role_granted_at')->nullable()->after('discord_role_granted_at');
16+
});
17+
}
18+
19+
public function down(): void
20+
{
21+
Schema::table('users', function (Blueprint $table) {
22+
$table->dropColumn('discord_early_adopter_role_granted_at');
23+
});
24+
}
25+
};

resources/views/livewire/customer/integrations.blade.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<div class="mt-4 prose dark:prose-invert prose-sm max-w-none">
3030
<ul class="list-disc list-inside space-y-2">
3131
<li><strong>GitHub:</strong> Max license holders can access the private <code>nativephp/mobile</code> repository. Plugin Dev Kit license holders and Ultra subscribers can access <code>nativephp/claude-code</code>.</li>
32-
<li><strong>Discord:</strong> Max license holders receive a special "Max" role in the NativePHP Discord server.</li>
32+
<li><strong>Discord:</strong> Max license holders receive a special "Max" role in the NativePHP Discord server. Early Access Program customers receive the "Early Adopter" role.</li>
3333
</ul>
3434
<p class="mt-4">
3535
Need help? Join our <a href="https://discord.gg/nativephp" target="_blank" class="text-blue-600 hover:underline dark:text-blue-400">Discord community</a>.
@@ -47,7 +47,7 @@
4747
<livewire:git-hub-access-banner :inline="true" />
4848
@endif
4949

50-
@if(auth()->user()->hasMaxAccess())
50+
@if(auth()->user()->isEapCustomer())
5151
<livewire:discord-access-banner :inline="true" />
5252
@endif
5353
</div>

0 commit comments

Comments
 (0)