Skip to content

Commit d7806fd

Browse files
shanerbaner82claude
andcommitted
Self-serve GitHub disconnect with confirmation on Integrations page
All users can now disconnect their GitHub account from the Integrations page, not just Max license holders. The disconnect goes through a confirmation modal that explains the consequences (private repo access revoked, plugin repo listing disabled) and reminds users to grant org access when re-authorizing. Disconnecting also clears the cached collaborator statuses so the page reflects the change immediately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5b18797 commit d7806fd

4 files changed

Lines changed: 132 additions & 7 deletions

File tree

‎app/Http/Controllers/GitHubIntegrationController.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Http\JsonResponse;
1010
use Illuminate\Http\RedirectResponse;
1111
use Illuminate\Support\Facades\Auth;
12+
use Illuminate\Support\Facades\Cache;
1213
use Illuminate\Support\Facades\Log;
1314
use Illuminate\Support\Str;
1415
use Laravel\Socialite\Facades\Socialite;
@@ -206,6 +207,9 @@ public function disconnect(): RedirectResponse
206207
'claude_plugins_repo_access_granted_at' => null,
207208
]);
208209

210+
Cache::forget("github_collab_status_{$user->id}");
211+
Cache::forget("github_claude_plugins_collab_status_{$user->id}");
212+
209213
return back()->with('success', 'GitHub account disconnected successfully.');
210214
}
211215

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,76 @@
3737
</div>
3838
</flux:card>
3939

40+
{{-- GitHub Account --}}
41+
<flux:card class="mb-6">
42+
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
43+
<div class="flex items-start gap-4">
44+
<svg class="h-6 w-6 flex-shrink-0 text-gray-700 dark:text-gray-300" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
45+
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
46+
</svg>
47+
<div>
48+
<flux:heading>GitHub Account</flux:heading>
49+
@if (auth()->user()->github_username)
50+
<flux:text class="mt-1">Connected as <span class="font-mono font-medium text-gray-900 dark:text-white">{{ '@' . auth()->user()->github_username }}</span></flux:text>
51+
@else
52+
<flux:text class="mt-1">Connect your GitHub account to access private repositories and publish plugins.</flux:text>
53+
@endif
54+
</div>
55+
</div>
56+
<div class="lg:flex-shrink-0">
57+
@if (auth()->user()->github_username)
58+
<flux:modal.trigger name="disconnect-github">
59+
<flux:button variant="danger">Disconnect</flux:button>
60+
</flux:modal.trigger>
61+
@else
62+
<flux:button href="{{ route('github.redirect', ['return' => route('customer.integrations')]) }}">Connect GitHub</flux:button>
63+
@endif
64+
</div>
65+
</div>
66+
</flux:card>
67+
68+
{{-- Disconnect GitHub Confirmation Modal --}}
69+
@if (auth()->user()->github_username)
70+
<flux:modal name="disconnect-github" class="md:w-[32rem]">
71+
<form action="{{ route('github.disconnect') }}" method="POST">
72+
@csrf
73+
@method('DELETE')
74+
75+
<flux:heading size="lg">Disconnect GitHub?</flux:heading>
76+
77+
<flux:text class="mt-2">
78+
This will unlink <span class="font-mono font-medium">{{ '@' . auth()->user()->github_username }}</span> from your NativePHP account.
79+
</flux:text>
80+
81+
<flux:callout variant="warning" icon="exclamation-triangle" class="mt-4">
82+
<flux:callout.text>
83+
<ul class="list-disc list-inside space-y-1">
84+
<li>Any access to the private <code>nativephp/mobile</code> and <code>nativephp/claude-code</code> repositories will be revoked.</li>
85+
<li>Your GitHub repositories will no longer be available when submitting or managing plugins.</li>
86+
<li>You can reconnect at any time. When re-authorizing, be sure to grant access to any organizations whose repositories you need.</li>
87+
</ul>
88+
</flux:callout.text>
89+
</flux:callout>
90+
91+
@if (! auth()->user()->password)
92+
<flux:callout variant="danger" icon="exclamation-triangle" class="mt-4">
93+
<flux:callout.text>
94+
Your account uses GitHub to sign in and has no password set. Consider setting a password in
95+
<a href="{{ route('customer.settings') }}" class="underline">Settings</a> before disconnecting.
96+
</flux:callout.text>
97+
</flux:callout>
98+
@endif
99+
100+
<div class="mt-6 flex justify-end gap-3">
101+
<flux:modal.close>
102+
<flux:button type="button" variant="ghost">Cancel</flux:button>
103+
</flux:modal.close>
104+
<flux:button type="submit" variant="danger">Disconnect GitHub</flux:button>
105+
</div>
106+
</form>
107+
</flux:modal>
108+
@endif
109+
40110
{{-- Claude Plugins Access --}}
41111
<div class="mb-6">
42112
<livewire:claude-plugins-access-banner :inline="true" />

‎resources/views/livewire/git-hub-access-banner.blade.php‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,6 @@
5555
</button>
5656
</form>
5757
@endif
58-
<form action="{{ route('github.disconnect') }}" method="POST">
59-
@csrf
60-
@method('DELETE')
61-
<button type="submit" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
62-
Disconnect
63-
</button>
64-
</form>
6558
@else
6659
<a href="{{ route('github.redirect') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gray-900 dark:bg-white dark:text-gray-900 hover:bg-gray-800 dark:hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
6760
Connect GitHub

‎tests/Feature/GitHubIntegrationTest.php‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,64 @@ public function test_user_cannot_request_repo_access_without_connected_github():
154154
$response->assertSessionHas('error', 'Please connect your GitHub account first.');
155155
}
156156

157+
public function test_user_without_any_license_sees_github_account_card_with_connect_button(): void
158+
{
159+
Http::fake(['api.github.com/*' => Http::response([], 404)]);
160+
161+
$user = User::factory()->create();
162+
163+
$response = $this->actingAs($user)->get('/dashboard/integrations');
164+
165+
$response->assertStatus(200);
166+
$response->assertSee('GitHub Account');
167+
$response->assertSee('Connect GitHub');
168+
}
169+
170+
public function test_connected_user_without_any_license_can_see_disconnect_option(): void
171+
{
172+
Http::fake(['api.github.com/*' => Http::response([], 404)]);
173+
174+
$user = User::factory()->create([
175+
'github_username' => 'testuser',
176+
'github_id' => '123456',
177+
]);
178+
179+
$response = $this->actingAs($user)->get('/dashboard/integrations');
180+
181+
$response->assertStatus(200);
182+
$response->assertSee('GitHub Account');
183+
$response->assertSee('Connected as');
184+
$response->assertSee('@testuser');
185+
$response->assertSee('Disconnect GitHub?');
186+
}
187+
188+
public function test_disconnect_clears_token_and_collaborator_status_caches(): void
189+
{
190+
Http::fake(['api.github.com/*' => Http::response([], 204)]);
191+
192+
$user = User::factory()->create([
193+
'github_username' => 'testuser',
194+
'github_id' => '123456',
195+
'github_token' => encrypt('gho_test_token'),
196+
]);
197+
198+
Cache::put("github_collab_status_{$user->id}", 'active', 300);
199+
Cache::put("github_claude_plugins_collab_status_{$user->id}", 'active', 300);
200+
201+
$response = $this->actingAs($user)
202+
->delete('/dashboard/github/disconnect');
203+
204+
$response->assertRedirect();
205+
$response->assertSessionHas('success');
206+
207+
$user->refresh();
208+
$this->assertNull($user->github_token);
209+
$this->assertNull($user->github_username);
210+
$this->assertNull($user->github_id);
211+
$this->assertFalse(Cache::has("github_collab_status_{$user->id}"));
212+
$this->assertFalse(Cache::has("github_claude_plugins_collab_status_{$user->id}"));
213+
}
214+
157215
public function test_user_can_disconnect_github_account(): void
158216
{
159217
Http::fake([

0 commit comments

Comments
 (0)