Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions app/Http/Controllers/DiscordIntegrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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.');
Expand Down
45 changes: 45 additions & 0 deletions app/Livewire/DiscordAccessBanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +28,7 @@ public function checkRoleStatus(): void

if (! $user || ! $user->discord_id) {
$this->hasMaxRole = false;
$this->hasEarlyAdopterRole = false;
$this->isGuildMember = false;

return;
Expand All @@ -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
Expand Down Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
}
}
50 changes: 41 additions & 9 deletions app/Support/DiscordApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ 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
{
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', '')
);
}

Expand Down Expand Up @@ -70,18 +72,48 @@ 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(
'%s/guilds/%s/members/%s/roles/%s',
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(),
Expand All @@ -93,19 +125,19 @@ 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(
'%s/guilds/%s/members/%s/roles/%s',
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(),
Expand All @@ -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(
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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');
});
}
};
4 changes: 2 additions & 2 deletions resources/views/livewire/customer/integrations.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<div class="mt-4 prose dark:prose-invert prose-sm max-w-none">
<ul class="list-disc list-inside space-y-2">
<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>
<li><strong>Discord:</strong> Max license holders receive a special "Max" role in the NativePHP Discord server.</li>
<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>
</ul>
<p class="mt-4">
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>.
Expand All @@ -47,7 +47,7 @@
<livewire:git-hub-access-banner :inline="true" />
@endif

@if(auth()->user()->hasMaxAccess())
@if(auth()->user()->isEapCustomer())
<livewire:discord-access-banner :inline="true" />
@endif
</div>
Expand Down
Loading
Loading