From d5a009909cb66e3f3b6b55c369b9f76d5598d69b Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 18 May 2026 16:07:26 +0100 Subject: [PATCH 1/2] Add Discord Early Adopter role for EAP customers Mirrors the existing Max role flow: EAP customers (users with licenses created before June 1, 2025) can now receive the Early Adopter Discord role automatically when connecting their Discord account, or via the request button on the access banner. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DiscordIntegrationController.php | 39 ++- app/Livewire/DiscordAccessBanner.php | 45 ++++ app/Models/User.php | 1 + app/Support/DiscordApi.php | 50 +++- config/services.php | 1 + ...adopter_role_granted_at_to_users_table.php | 25 ++ .../livewire/discord-access-banner.blade.php | 67 +++-- tests/Feature/DiscordIntegrationTest.php | 191 +++++++++++++ .../Livewire/DiscordAccessBannerTest.php | 251 ++++++++++++++++++ 9 files changed, 625 insertions(+), 45 deletions(-) create mode 100644 database/migrations/2026_04_17_154933_add_discord_early_adopter_role_granted_at_to_users_table.php create mode 100644 tests/Feature/DiscordIntegrationTest.php create mode 100644 tests/Feature/Livewire/DiscordAccessBannerTest.php 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/discord-access-banner.blade.php b/resources/views/livewire/discord-access-banner.blade.php index f542db93..e0467d3b 100644 --- a/resources/views/livewire/discord-access-banner.blade.php +++ b/resources/views/livewire/discord-access-banner.blade.php @@ -10,7 +10,7 @@

- Discord Max Role + Discord Roles

@if(auth()->user()->discord_username) @@ -22,32 +22,38 @@ Not in Server

- @elseif($hasMaxRole) -

- - Max Role Active - -

- @elseif(auth()->user()->hasMaxAccess()) -

- - Eligible - -

+ @else +
+ @if($hasMaxRole) + + Max Role Active + + @elseif(auth()->user()->hasMaxAccess()) + + Max Eligible + + @endif + + @if($hasEarlyAdopterRole) + + Early Adopter Active + + @elseif(auth()->user()->isEapCustomer()) + + Early Adopter Eligible + + @endif +
@endif @else -

Connect your Discord account to receive the Max role.

+

Connect your Discord account to receive your roles.

@endif
@if(auth()->user()->discord_username) - @if($hasMaxRole) - - Open Discord - - @elseif(!$isGuildMember) + @if(!$isGuildMember) Join Discord Server @@ -55,11 +61,24 @@ Check Status Checking... - @elseif(auth()->user()->hasMaxAccess()) - + @else + @if(!$hasMaxRole && auth()->user()->hasMaxAccess()) + + @endif + @if(!$hasEarlyAdopterRole && auth()->user()->isEapCustomer()) + + @endif + @if(($hasMaxRole || !auth()->user()->hasMaxAccess()) && ($hasEarlyAdopterRole || !auth()->user()->isEapCustomer())) + + Open Discord + + @endif @endif
@csrf diff --git a/tests/Feature/DiscordIntegrationTest.php b/tests/Feature/DiscordIntegrationTest.php new file mode 100644 index 00000000..05ecaef6 --- /dev/null +++ b/tests/Feature/DiscordIntegrationTest.php @@ -0,0 +1,191 @@ + 'test-client-id', + 'services.discord.client_secret' => 'test-client-secret', + 'services.discord.bot_token' => 'test-bot-token', + 'services.discord.guild_id' => 'test-guild-id', + 'services.discord.max_role_id' => 'max-role-id', + 'services.discord.early_adopter_role_id' => 'early-adopter-role-id', + ]); + } + + #[Test] + public function callback_assigns_early_adopter_role_for_eap_customer(): void + { + $user = User::factory()->create(); + + License::factory() + ->for($user) + ->eapEligible() + ->active() + ->withoutSubscriptionItem() + ->create(); + + Http::fake([ + 'discord.com/api/oauth2/token' => Http::response([ + 'access_token' => 'test-access-token', + ]), + 'discord.com/api/v10/users/@me' => Http::response([ + 'id' => '999888777', + 'username' => 'eapuser', + ]), + 'discord.com/api/v10/guilds/test-guild-id/members/999888777' => Http::response([ + 'roles' => [], + ], 200), + 'discord.com/api/v10/guilds/test-guild-id/members/999888777/roles/early-adopter-role-id' => Http::response([], 204), + ]); + + $response = $this->actingAs($user) + ->get('/auth/discord/callback?code=test-code'); + + $response->assertRedirect(route('customer.integrations')); + $response->assertSessionHas('success'); + + $user->refresh(); + $this->assertEquals('999888777', $user->discord_id); + $this->assertEquals('eapuser', $user->discord_username); + $this->assertNotNull($user->discord_early_adopter_role_granted_at); + } + + #[Test] + public function callback_assigns_both_roles_for_eap_customer_with_max_access(): void + { + $user = User::factory()->create(); + + License::factory() + ->for($user) + ->max() + ->eapEligible() + ->active() + ->withoutSubscriptionItem() + ->create(); + + Http::fake([ + 'discord.com/api/oauth2/token' => Http::response([ + 'access_token' => 'test-access-token', + ]), + 'discord.com/api/v10/users/@me' => Http::response([ + 'id' => '999888777', + 'username' => 'maxeapuser', + ]), + 'discord.com/api/v10/guilds/test-guild-id/members/999888777' => Http::response([ + 'roles' => [], + ], 200), + 'discord.com/api/v10/guilds/test-guild-id/members/999888777/roles/*' => Http::response([], 204), + ]); + + $response = $this->actingAs($user) + ->get('/auth/discord/callback?code=test-code'); + + $response->assertRedirect(route('customer.integrations')); + $response->assertSessionHas('success'); + + $user->refresh(); + $this->assertNotNull($user->discord_role_granted_at); + $this->assertNotNull($user->discord_early_adopter_role_granted_at); + } + + #[Test] + public function callback_does_not_assign_early_adopter_role_for_non_eap_customer(): void + { + $user = User::factory()->create(); + + License::factory() + ->for($user) + ->afterEap() + ->active() + ->withoutSubscriptionItem() + ->create(); + + Http::fake([ + 'discord.com/api/oauth2/token' => Http::response([ + 'access_token' => 'test-access-token', + ]), + 'discord.com/api/v10/users/@me' => Http::response([ + 'id' => '999888777', + 'username' => 'newuser', + ]), + 'discord.com/api/v10/guilds/test-guild-id/members/999888777' => Http::response([ + 'roles' => [], + ], 200), + ]); + + $response = $this->actingAs($user) + ->get('/auth/discord/callback?code=test-code'); + + $response->assertRedirect(route('customer.integrations')); + + $user->refresh(); + $this->assertNull($user->discord_early_adopter_role_granted_at); + } + + #[Test] + public function disconnect_removes_early_adopter_role(): void + { + $user = User::factory()->create([ + 'discord_id' => '999888777', + 'discord_username' => 'testuser', + 'discord_role_granted_at' => now(), + 'discord_early_adopter_role_granted_at' => now(), + ]); + + Http::fake([ + 'discord.com/api/v10/guilds/test-guild-id/members/999888777/roles/*' => Http::response([], 204), + ]); + + $response = $this->actingAs($user) + ->delete('/dashboard/discord/disconnect'); + + $response->assertSessionHas('success', 'Discord account disconnected successfully.'); + + $user->refresh(); + $this->assertNull($user->discord_id); + $this->assertNull($user->discord_username); + $this->assertNull($user->discord_role_granted_at); + $this->assertNull($user->discord_early_adopter_role_granted_at); + } + + #[Test] + public function disconnect_only_removes_early_adopter_role_when_max_role_was_not_granted(): void + { + $user = User::factory()->create([ + 'discord_id' => '999888777', + 'discord_username' => 'testuser', + 'discord_role_granted_at' => null, + 'discord_early_adopter_role_granted_at' => now(), + ]); + + Http::fake([ + 'discord.com/api/v10/guilds/test-guild-id/members/999888777/roles/early-adopter-role-id' => Http::response([], 204), + ]); + + $response = $this->actingAs($user) + ->delete('/dashboard/discord/disconnect'); + + $response->assertSessionHas('success', 'Discord account disconnected successfully.'); + + $user->refresh(); + $this->assertNull($user->discord_id); + $this->assertNull($user->discord_early_adopter_role_granted_at); + + Http::assertSentCount(1); + } +} diff --git a/tests/Feature/Livewire/DiscordAccessBannerTest.php b/tests/Feature/Livewire/DiscordAccessBannerTest.php new file mode 100644 index 00000000..772e96b7 --- /dev/null +++ b/tests/Feature/Livewire/DiscordAccessBannerTest.php @@ -0,0 +1,251 @@ + 'test-bot-token', + 'services.discord.guild_id' => 'test-guild-id', + 'services.discord.max_role_id' => 'max-role-id', + 'services.discord.early_adopter_role_id' => 'early-adopter-role-id', + ]); + } + + #[Test] + public function it_shows_not_connected_state_when_discord_is_not_linked(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Connect your Discord account to receive your roles.'); + } + + #[Test] + public function it_shows_connected_username_when_discord_is_linked(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + $this->fakeDiscordApi(isGuildMember: true); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('testuser'); + } + + #[Test] + public function it_shows_not_in_server_when_user_is_not_a_guild_member(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + $this->fakeDiscordApi(isGuildMember: false); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Not in Server'); + } + + #[Test] + public function it_shows_max_role_active_when_user_has_max_role(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + $this->fakeDiscordApi(isGuildMember: true, roles: ['max-role-id']); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Max Role Active'); + } + + #[Test] + public function it_shows_early_adopter_active_when_user_has_early_adopter_role(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + $this->fakeDiscordApi(isGuildMember: true, roles: ['early-adopter-role-id']); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Early Adopter Active'); + } + + #[Test] + public function it_shows_early_adopter_eligible_for_eap_customer_without_role(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + License::factory() + ->for($user) + ->eapEligible() + ->active() + ->withoutSubscriptionItem() + ->create(); + + $this->fakeDiscordApi(isGuildMember: true); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Early Adopter Eligible'); + } + + #[Test] + public function it_shows_request_early_adopter_role_button_for_eligible_user(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + License::factory() + ->for($user) + ->eapEligible() + ->active() + ->withoutSubscriptionItem() + ->create(); + + $this->fakeDiscordApi(isGuildMember: true); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Request Early Adopter Role'); + } + + #[Test] + public function it_assigns_early_adopter_role_on_request(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + License::factory() + ->for($user) + ->eapEligible() + ->active() + ->withoutSubscriptionItem() + ->create(); + + $this->fakeDiscordApi(isGuildMember: true); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->call('requestEarlyAdopterRole') + ->assertHasNoErrors(); + + $this->assertNotNull($user->fresh()->discord_early_adopter_role_granted_at); + } + + #[Test] + public function it_does_not_assign_early_adopter_role_for_non_eap_customer(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + // License created after EAP cutoff + License::factory() + ->for($user) + ->afterEap() + ->active() + ->withoutSubscriptionItem() + ->create(); + + $this->fakeDiscordApi(isGuildMember: true); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->call('requestEarlyAdopterRole'); + + $this->assertNull($user->fresh()->discord_early_adopter_role_granted_at); + } + + #[Test] + public function it_does_not_assign_early_adopter_role_when_not_guild_member(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + License::factory() + ->for($user) + ->eapEligible() + ->active() + ->withoutSubscriptionItem() + ->create(); + + $this->fakeDiscordApi(isGuildMember: false); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->call('requestEarlyAdopterRole'); + + $this->assertNull($user->fresh()->discord_early_adopter_role_granted_at); + } + + #[Test] + public function it_shows_both_roles_active_when_user_has_both(): void + { + $user = User::factory()->create([ + 'discord_id' => '123456789', + 'discord_username' => 'testuser', + ]); + + $this->fakeDiscordApi(isGuildMember: true, roles: ['max-role-id', 'early-adopter-role-id']); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->assertSee('Max Role Active') + ->assertSee('Early Adopter Active'); + } + + /** + * @param array $roles + */ + private function fakeDiscordApi(bool $isGuildMember, array $roles = []): void + { + Cache::flush(); + + $memberResponse = $isGuildMember + ? Http::response(['roles' => $roles], 200) + : Http::response([], 404); + + Http::fake([ + 'discord.com/api/v10/guilds/test-guild-id/members/*/roles/*' => Http::response([], 204), + 'discord.com/api/v10/guilds/test-guild-id/members/*' => $memberResponse, + ]); + } +} From 7e8896f66e726e0cf0e99d8ba99c8aefa62ffdb4 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 18 May 2026 16:10:58 +0100 Subject: [PATCH 2/2] Show Discord integration banner to Early Access Program customers Co-Authored-By: Claude Opus 4.7 (1M context) --- resources/views/livewire/customer/integrations.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 @@
  • GitHub: Max license holders can access the private nativephp/mobile repository. Plugin Dev Kit license holders and Ultra subscribers can access nativephp/claude-code.
  • -
  • Discord: Max license holders receive a special "Max" role in the NativePHP Discord server.
  • +
  • Discord: Max license holders receive a special "Max" role in the NativePHP Discord server. Early Access Program customers receive the "Early Adopter" role.

Need help? Join our Discord community. @@ -47,7 +47,7 @@ @endif - @if(auth()->user()->hasMaxAccess()) + @if(auth()->user()->isEapCustomer()) @endif