Skip to content
Draft
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
12 changes: 12 additions & 0 deletions app/Http/Controllers/DiscordIntegrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ public function handleCallback(): RedirectResponse
}
}

if ($user->hasPurchasedMasterclass()) {
if ($discord->assignMasterRole($discordUser['id'])) {
$user->update(['discord_master_role_granted_at' => now()]);
$rolesAssigned[] = 'Master';
}
}

if (count($rolesAssigned) > 0) {
$roleNames = implode(' and ', $rolesAssigned);

Expand Down Expand Up @@ -123,13 +130,18 @@ public function disconnect(): RedirectResponse
if ($user->discord_early_adopter_role_granted_at) {
$discord->removeEarlyAdopterRole($user->discord_id);
}

if ($user->discord_master_role_granted_at) {
$discord->removeMasterRole($user->discord_id);
}
}

$user->update([
'discord_id' => null,
'discord_username' => null,
'discord_role_granted_at' => null,
'discord_early_adopter_role_granted_at' => null,
'discord_master_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 @@ -14,6 +14,8 @@ class DiscordAccessBanner extends Component

public bool $hasEarlyAdopterRole = false;

public bool $hasMasterRole = false;

public bool $isGuildMember = false;

public function mount(bool $inline = false): void
Expand All @@ -29,6 +31,7 @@ public function checkRoleStatus(): void
if (! $user || ! $user->discord_id) {
$this->hasUltraRole = false;
$this->hasEarlyAdopterRole = false;
$this->hasMasterRole = false;
$this->isGuildMember = false;

return;
Expand All @@ -43,12 +46,14 @@ public function checkRoleStatus(): void
'isGuildMember' => $discord->isGuildMember($user->discord_id),
'hasUltraRole' => $discord->hasUltraRole($user->discord_id),
'hasEarlyAdopterRole' => $discord->hasEarlyAdopterRole($user->discord_id),
'hasMasterRole' => $discord->hasMasterRole($user->discord_id),
];
});

$this->isGuildMember = $status['isGuildMember'];
$this->hasUltraRole = $status['hasUltraRole'];
$this->hasEarlyAdopterRole = $status['hasEarlyAdopterRole'];
$this->hasMasterRole = $status['hasMasterRole'] ?? false;

if ($this->hasUltraRole && ! $user->discord_role_granted_at) {
$user->update(['discord_role_granted_at' => now()]);
Expand All @@ -57,6 +62,10 @@ public function checkRoleStatus(): void
if ($this->hasEarlyAdopterRole && ! $user->discord_early_adopter_role_granted_at) {
$user->update(['discord_early_adopter_role_granted_at' => now()]);
}

if ($this->hasMasterRole && ! $user->discord_master_role_granted_at) {
$user->update(['discord_master_role_granted_at' => now()]);
}
}

public function refreshStatus(): void
Expand Down Expand Up @@ -142,6 +151,42 @@ public function requestEarlyAdopterRole(): void
}
}

public function requestMasterRole(): void
{
$user = auth()->user();

if (! $user || ! $user->discord_id) {
session()->flash('error', 'Please connect your Discord account first.');

return;
}

if (! $user->hasPurchasedMasterclass()) {
session()->flash('error', 'The Master role is for Masterclass students.');

return;
}

$discord = DiscordApi::make();

if (! $discord->isGuildMember($user->discord_id)) {
session()->flash('error', 'Please join the NativePHP Discord server first.');

return;
}

$success = $discord->assignMasterRole($user->discord_id);

if ($success) {
$user->update(['discord_master_role_granted_at' => now()]);
Cache::forget("discord_role_status_{$user->id}");
$this->checkRoleStatus();
session()->flash('success', 'Master role assigned successfully!');
} else {
session()->flash('error', 'Failed to assign Master role. Please try again later.');
}
}

public function render()
{
return view('livewire.discord-access-banner');
Expand Down
15 changes: 15 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Filament\Models\Contracts\HasName;
use Filament\Panel;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
Expand Down Expand Up @@ -128,6 +129,19 @@ public function hasProductLicense(Product $product): bool
return $this->hasProductAccessViaTeam($product);
}

/**
* Check if user has purchased The NativePHP Masterclass.
*
* Mirrors the direct-ownership check used to gate the course itself, so the
* Discord Master role tracks course access rather than team membership.
*/
public function hasPurchasedMasterclass(): bool
{
return $this->productLicenses()
->whereHas('product', fn (Builder $query) => $query->where('slug', 'nativephp-masterclass'))
->exists();
}

/**
* @return HasMany<LessonProgress>
*/
Expand Down Expand Up @@ -579,6 +593,7 @@ protected function casts(): array
'claude_plugins_repo_access_granted_at' => 'datetime',
'discord_role_granted_at' => 'datetime',
'discord_early_adopter_role_granted_at' => 'datetime',
'discord_master_role_granted_at' => 'datetime',
];
}
}
21 changes: 19 additions & 2 deletions app/Support/DiscordApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public function __construct(
private ?string $botToken,
private ?string $guildId,
private ?string $ultraRoleId,
private ?string $earlyAdopterRoleId
private ?string $earlyAdopterRoleId,
private ?string $masterRoleId = null
) {}

public static function make(): static
Expand All @@ -22,7 +23,8 @@ public static function make(): static
config('services.discord.bot_token', ''),
config('services.discord.guild_id', ''),
config('services.discord.ultra_role_id', ''),
config('services.discord.early_adopter_role_id', '')
config('services.discord.early_adopter_role_id', ''),
config('services.discord.master_role_id', '')
);
}

Expand Down Expand Up @@ -101,6 +103,21 @@ public function hasEarlyAdopterRole(string $discordUserId): bool
return $this->hasRole($discordUserId, $this->earlyAdopterRoleId);
}

public function assignMasterRole(string $discordUserId): bool
{
return $this->assignRole($discordUserId, $this->masterRoleId, 'Master');
}

public function removeMasterRole(string $discordUserId): bool
{
return $this->removeRole($discordUserId, $this->masterRoleId, 'Master');
}

public function hasMasterRole(string $discordUserId): bool
{
return $this->hasRole($discordUserId, $this->masterRoleId);
}

private function assignRole(string $discordUserId, ?string $roleId, string $roleName): bool
{
$response = Http::withToken($this->botToken, 'Bot')
Expand Down
1 change: 1 addition & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
'guild_id' => env('DISCORD_GUILD_ID'),
'ultra_role_id' => env('DISCORD_ULTRA_ROLE_ID'),
'early_adopter_role_id' => env('DISCORD_EARLY_ADOPTER_ROLE_ID'),
'master_role_id' => env('DISCORD_MASTER_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_master_role_granted_at')->nullable()->after('discord_early_adopter_role_granted_at');
});
}

public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('discord_master_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 and Ultra subscribers receive a special "Ultra" role in the NativePHP Discord server. Early Access Program customers receive the "Early Adopter" role.</li>
<li><strong>Discord:</strong> Max license holders and Ultra subscribers receive a special "Ultra" role in the NativePHP Discord server. Early Access Program customers receive the "Early Adopter" role. Masterclass students receive the "Master" role, unlocking private channels.</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 Down Expand Up @@ -117,7 +117,7 @@
<livewire:git-hub-access-banner :inline="true" />
@endif

@if(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess() || auth()->user()->isEapCustomer())
@if(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess() || auth()->user()->isEapCustomer() || auth()->user()->hasPurchasedMasterclass())
<livewire:discord-access-banner :inline="true" />
@endif
</div>
Expand Down
18 changes: 17 additions & 1 deletion resources/views/livewire/discord-access-banner.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@
Early Adopter Eligible
</span>
@endif

@if($hasMasterRole)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Master Role Active
</span>
@elseif(auth()->user()->hasPurchasedMasterclass())
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
Master Eligible
</span>
@endif
</div>
@endif
@else
Expand Down Expand Up @@ -74,7 +84,13 @@
<span wire:loading wire:target="requestEarlyAdopterRole">Requesting...</span>
</button>
@endif
@if(($hasUltraRole || !(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess())) && ($hasEarlyAdopterRole || !auth()->user()->isEapCustomer()))
@if(!$hasMasterRole && auth()->user()->hasPurchasedMasterclass())
<button wire:click="requestMasterRole" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span wire:loading.remove wire:target="requestMasterRole">Request Master Role</span>
<span wire:loading wire:target="requestMasterRole">Requesting...</span>
</button>
@endif
@if(($hasUltraRole || !(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess())) && ($hasEarlyAdopterRole || !auth()->user()->isEapCustomer()) && ($hasMasterRole || !auth()->user()->hasPurchasedMasterclass()))
<a href="https://discord.gg/nativephp" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Open Discord
</a>
Expand Down
Loading