diff --git a/app/Http/Controllers/DiscordIntegrationController.php b/app/Http/Controllers/DiscordIntegrationController.php index 9abad300..23b886f8 100644 --- a/app/Http/Controllers/DiscordIntegrationController.php +++ b/app/Http/Controllers/DiscordIntegrationController.php @@ -70,23 +70,30 @@ public function handleCallback(): RedirectResponse if (! $discord->isGuildMember($discordUser['id'])) { return to_route('customer.integrations') - ->with('warning', 'Discord account connected! Please join the NativePHP Discord server to receive the Max role.'); + ->with('warning', 'Discord account connected! Please join the NativePHP Discord server to receive your roles.'); } - if ($user->hasMaxAccess()) { - $success = $discord->assignMaxRole($discordUser['id']); + $rolesAssigned = []; - if ($success) { - $user->update([ - 'discord_role_granted_at' => now(), - ]); + if ($user->hasMaxAccess()) { + if ($discord->assignMaxRole($discordUser['id'])) { + $user->update(['discord_role_granted_at' => now()]); + $rolesAssigned[] = 'Max'; + } + } - return to_route('customer.integrations') - ->with('success', 'Discord account connected and Max role assigned!'); + if ($user->isEapCustomer()) { + if ($discord->assignEarlyAdopterRole($discordUser['id'])) { + $user->update(['discord_early_adopter_role_granted_at' => now()]); + $rolesAssigned[] = 'Early Adopter'; } + } + + if (count($rolesAssigned) > 0) { + $roleNames = implode(' and ', $rolesAssigned); return to_route('customer.integrations') - ->with('warning', 'Discord account connected, but we could not assign the Max role. Please try again later.'); + ->with('success', "Discord account connected and {$roleNames} role(s) assigned!"); } return to_route('customer.integrations') @@ -106,15 +113,23 @@ public function disconnect(): RedirectResponse { $user = Auth::user(); - if ($user->discord_role_granted_at && $user->discord_id) { + if ($user->discord_id) { $discord = DiscordApi::make(); - $discord->removeMaxRole($user->discord_id); + + if ($user->discord_role_granted_at) { + $discord->removeMaxRole($user->discord_id); + } + + if ($user->discord_early_adopter_role_granted_at) { + $discord->removeEarlyAdopterRole($user->discord_id); + } } $user->update([ 'discord_id' => null, 'discord_username' => null, 'discord_role_granted_at' => null, + 'discord_early_adopter_role_granted_at' => null, ]); return back()->with('success', 'Discord account disconnected successfully.'); diff --git a/app/Livewire/DiscordAccessBanner.php b/app/Livewire/DiscordAccessBanner.php index dd882880..8e3fb501 100644 --- a/app/Livewire/DiscordAccessBanner.php +++ b/app/Livewire/DiscordAccessBanner.php @@ -12,6 +12,8 @@ class DiscordAccessBanner extends Component public bool $hasMaxRole = false; + public bool $hasEarlyAdopterRole = false; + public bool $isGuildMember = false; public function mount(bool $inline = false): void @@ -26,6 +28,7 @@ public function checkRoleStatus(): void if (! $user || ! $user->discord_id) { $this->hasMaxRole = false; + $this->hasEarlyAdopterRole = false; $this->isGuildMember = false; return; @@ -39,15 +42,21 @@ public function checkRoleStatus(): void return [ 'isGuildMember' => $discord->isGuildMember($user->discord_id), 'hasMaxRole' => $discord->hasMaxRole($user->discord_id), + 'hasEarlyAdopterRole' => $discord->hasEarlyAdopterRole($user->discord_id), ]; }); $this->isGuildMember = $status['isGuildMember']; $this->hasMaxRole = $status['hasMaxRole']; + $this->hasEarlyAdopterRole = $status['hasEarlyAdopterRole']; if ($this->hasMaxRole && ! $user->discord_role_granted_at) { $user->update(['discord_role_granted_at' => now()]); } + + if ($this->hasEarlyAdopterRole && ! $user->discord_early_adopter_role_granted_at) { + $user->update(['discord_early_adopter_role_granted_at' => now()]); + } } public function refreshStatus(): void @@ -97,6 +106,42 @@ public function requestMaxRole(): void } } + public function requestEarlyAdopterRole(): void + { + $user = auth()->user(); + + if (! $user || ! $user->discord_id) { + session()->flash('error', 'Please connect your Discord account first.'); + + return; + } + + if (! $user->isEapCustomer()) { + session()->flash('error', 'The Early Adopter role is for early access program customers.'); + + return; + } + + $discord = DiscordApi::make(); + + if (! $discord->isGuildMember($user->discord_id)) { + session()->flash('error', 'Please join the NativePHP Discord server first.'); + + return; + } + + $success = $discord->assignEarlyAdopterRole($user->discord_id); + + if ($success) { + $user->update(['discord_early_adopter_role_granted_at' => now()]); + Cache::forget("discord_role_status_{$user->id}"); + $this->checkRoleStatus(); + session()->flash('success', 'Early Adopter role assigned successfully!'); + } else { + session()->flash('error', 'Failed to assign Early Adopter role. Please try again later.'); + } + } + public function render() { return view('livewire.discord-access-banner'); diff --git a/app/Models/User.php b/app/Models/User.php index c8bfc783..455778bd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -570,6 +570,7 @@ protected function casts(): array 'mobile_repo_access_granted_at' => 'datetime', 'claude_plugins_repo_access_granted_at' => 'datetime', 'discord_role_granted_at' => 'datetime', + 'discord_early_adopter_role_granted_at' => 'datetime', ]; } } diff --git a/app/Support/DiscordApi.php b/app/Support/DiscordApi.php index 07dabe38..35e20912 100644 --- a/app/Support/DiscordApi.php +++ b/app/Support/DiscordApi.php @@ -12,7 +12,8 @@ class DiscordApi public function __construct( private ?string $botToken, private ?string $guildId, - private ?string $maxRoleId + private ?string $maxRoleId, + private ?string $earlyAdopterRoleId ) {} public static function make(): static @@ -20,7 +21,8 @@ public static function make(): static return new static( config('services.discord.bot_token', ''), config('services.discord.guild_id', ''), - config('services.discord.max_role_id', '') + config('services.discord.max_role_id', ''), + config('services.discord.early_adopter_role_id', '') ); } @@ -70,6 +72,36 @@ public function isGuildMember(string $discordUserId): bool } public function assignMaxRole(string $discordUserId): bool + { + return $this->assignRole($discordUserId, $this->maxRoleId, 'Max'); + } + + public function removeMaxRole(string $discordUserId): bool + { + return $this->removeRole($discordUserId, $this->maxRoleId, 'Max'); + } + + public function hasMaxRole(string $discordUserId): bool + { + return $this->hasRole($discordUserId, $this->maxRoleId); + } + + public function assignEarlyAdopterRole(string $discordUserId): bool + { + return $this->assignRole($discordUserId, $this->earlyAdopterRoleId, 'Early Adopter'); + } + + public function removeEarlyAdopterRole(string $discordUserId): bool + { + return $this->removeRole($discordUserId, $this->earlyAdopterRoleId, 'Early Adopter'); + } + + public function hasEarlyAdopterRole(string $discordUserId): bool + { + return $this->hasRole($discordUserId, $this->earlyAdopterRoleId); + } + + private function assignRole(string $discordUserId, ?string $roleId, string $roleName): bool { $response = Http::withToken($this->botToken, 'Bot') ->put(sprintf( @@ -77,11 +109,11 @@ public function assignMaxRole(string $discordUserId): bool self::BASE_URL, $this->guildId, $discordUserId, - $this->maxRoleId + $roleId )); if ($response->failed()) { - Log::error('Failed to assign Discord Max role', [ + Log::error("Failed to assign Discord {$roleName} role", [ 'discord_user_id' => $discordUserId, 'status' => $response->status(), 'response' => $response->json(), @@ -93,7 +125,7 @@ public function assignMaxRole(string $discordUserId): bool return true; } - public function removeMaxRole(string $discordUserId): bool + private function removeRole(string $discordUserId, ?string $roleId, string $roleName): bool { $response = Http::withToken($this->botToken, 'Bot') ->delete(sprintf( @@ -101,11 +133,11 @@ public function removeMaxRole(string $discordUserId): bool self::BASE_URL, $this->guildId, $discordUserId, - $this->maxRoleId + $roleId )); if ($response->failed()) { - Log::error('Failed to remove Discord Max role', [ + Log::error("Failed to remove Discord {$roleName} role", [ 'discord_user_id' => $discordUserId, 'status' => $response->status(), 'response' => $response->json(), @@ -117,7 +149,7 @@ public function removeMaxRole(string $discordUserId): bool return true; } - public function hasMaxRole(string $discordUserId): bool + private function hasRole(string $discordUserId, ?string $roleId): bool { $response = Http::withToken($this->botToken, 'Bot') ->get(sprintf( @@ -140,6 +172,6 @@ public function hasMaxRole(string $discordUserId): bool $member = $response->json(); $roles = $member['roles'] ?? []; - return in_array($this->maxRoleId, $roles, true); + return in_array($roleId, $roles, true); } } diff --git a/config/services.php b/config/services.php index b9a0164b..ad8ebc06 100644 --- a/config/services.php +++ b/config/services.php @@ -57,6 +57,7 @@ 'bot_token' => env('DISCORD_BOT_TOKEN'), 'guild_id' => env('DISCORD_GUILD_ID'), 'max_role_id' => env('DISCORD_MAX_ROLE_ID'), + 'early_adopter_role_id' => env('DISCORD_EARLY_ADOPTER_ROLE_ID'), ], 'turnstile' => [ diff --git a/database/migrations/2026_04_17_154933_add_discord_early_adopter_role_granted_at_to_users_table.php b/database/migrations/2026_04_17_154933_add_discord_early_adopter_role_granted_at_to_users_table.php new file mode 100644 index 00000000..9066fde2 --- /dev/null +++ b/database/migrations/2026_04_17_154933_add_discord_early_adopter_role_granted_at_to_users_table.php @@ -0,0 +1,25 @@ +timestamp('discord_early_adopter_role_granted_at')->nullable()->after('discord_role_granted_at'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('discord_early_adopter_role_granted_at'); + }); + } +}; diff --git a/resources/views/livewire/customer/integrations.blade.php b/resources/views/livewire/customer/integrations.blade.php index 59919566..4cb80b0f 100644 --- a/resources/views/livewire/customer/integrations.blade.php +++ b/resources/views/livewire/customer/integrations.blade.php @@ -29,7 +29,7 @@
nativephp/mobile repository. Plugin Dev Kit license holders and Ultra subscribers can access nativephp/claude-code.
Need help? Join our Discord community.
@@ -47,7 +47,7 @@
- - Max Role Active - -
- @elseif(auth()->user()->hasMaxAccess()) -- - Eligible - -
+ @else +Connect your Discord account to receive the Max role.
+Connect your Discord account to receive your roles.
@endif