diff --git a/config/socialstream.php b/config/socialstream.php
index 95ae448a..c6277cde 100644
--- a/config/socialstream.php
+++ b/config/socialstream.php
@@ -6,6 +6,7 @@
return [
'middleware' => ['web'],
'prompt' => 'Or Login Via',
+ 'confirmation-prompt' => null,
'providers' => [
// Providers::github(),
],
diff --git a/resources/views/oauth/prompt.blade.php b/resources/views/oauth/prompt.blade.php
new file mode 100644
index 00000000..89511e1c
--- /dev/null
+++ b/resources/views/oauth/prompt.blade.php
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ Laravel
+
+
+
+
+
+
+
+
+
+
+
+
+ Confirm connection of your {{ \JoelButcher\Socialstream\Providers::name($provider) }} account.
+
+
+
+
+
+
+
diff --git a/src/Actions/AuthenticateOAuthCallback.php b/src/Actions/AuthenticateOAuthCallback.php
index f0831e90..99f36828 100644
--- a/src/Actions/AuthenticateOAuthCallback.php
+++ b/src/Actions/AuthenticateOAuthCallback.php
@@ -136,7 +136,7 @@ protected function login(Authenticatable $user, mixed $account, string $provider
/**
* Attempt to link the provider to the authenticated user.
*/
- private function linkProvider(Authenticatable $user, string $provider, ProviderUser $providerAccount): SocialstreamResponse
+ public function linkProvider(Authenticatable $user, string $provider, ProviderUser $providerAccount): SocialstreamResponse
{
$account = $this->findAccount($provider, $providerAccount);
diff --git a/src/Filament/web.php b/src/Filament/web.php
new file mode 100644
index 00000000..dcc6b596
--- /dev/null
+++ b/src/Filament/web.php
@@ -0,0 +1,12 @@
+ config('socialstream.middleware', ['web'])], function () {
+ Route::get('/oauth/{provider}/callback/prompt', [OAuthController::class, 'prompt'])->name('oauth.callback.prompt');
+ Route::post('/oauth/{provider}/callback/confirm', [OAuthController::class, 'confirm'])->name(
+ 'oauth.callback.confirm'
+ );
+});
diff --git a/src/HasProfilePhoto.php b/src/HasProfilePhoto.php
deleted file mode 100644
index 8fcc88d3..00000000
--- a/src/HasProfilePhoto.php
+++ /dev/null
@@ -1,89 +0,0 @@
-profile_photo_path, function ($previous) use ($photo, $storagePath) {
- $this->forceFill([
- 'profile_photo_path' => $photo->storePublicly(
- $storagePath, ['disk' => $this->profilePhotoDisk()]
- ),
- ])->save();
-
- if ($previous) {
- Storage::disk($this->profilePhotoDisk())->delete($previous);
- }
- });
- }
-
- /**
- * Delete the user's profile photo.
- *
- * @return void
- */
- public function deleteProfilePhoto()
- {
- if (! Features::managesProfilePhotos()) {
- return;
- }
-
- if (is_null($this->profile_photo_path)) {
- return;
- }
-
- Storage::disk($this->profilePhotoDisk())->delete($this->profile_photo_path);
-
- $this->forceFill([
- 'profile_photo_path' => null,
- ])->save();
- }
-
- /**
- * Get the URL to the user's profile photo.
- */
- public function profilePhotoUrl(): Attribute
- {
- return Attribute::get(function () {
- return $this->profile_photo_path
- ? Storage::disk($this->profilePhotoDisk())->url($this->profile_photo_path)
- : $this->defaultProfilePhotoUrl();
- });
- }
-
- /**
- * Get the default profile photo URL if no profile photo has been uploaded.
- *
- * @return string
- */
- protected function defaultProfilePhotoUrl()
- {
- $name = trim(collect(explode(' ', $this->name))->map(function ($segment) {
- return mb_substr($segment, 0, 1);
- })->join(' '));
-
- return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=7F9CF5&background=EBF4FF';
- }
-
- /**
- * Get the disk that profile photos should be stored on.
- *
- * @return string
- */
- protected function profilePhotoDisk()
- {
- return isset($_ENV['VAPOR_ARTIFACT_NAME']) ? 's3' : config('jetstream.profile_photo_disk', 'public');
- }
-}
diff --git a/src/Http/Controllers/OAuthController.php b/src/Http/Controllers/OAuthController.php
index 8b2b1f02..17911fed 100644
--- a/src/Http/Controllers/OAuthController.php
+++ b/src/Http/Controllers/OAuthController.php
@@ -2,16 +2,24 @@
namespace JoelButcher\Socialstream\Http\Controllers;
+use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
+use Illuminate\Support\Facades\Session;
+use Illuminate\Support\MessageBag;
+use Illuminate\Support\ViewErrorBag;
use JoelButcher\Socialstream\Contracts\AuthenticatesOAuthCallback;
use JoelButcher\Socialstream\Contracts\GeneratesProviderRedirect;
use JoelButcher\Socialstream\Contracts\HandlesInvalidState;
use JoelButcher\Socialstream\Contracts\HandlesOAuthCallbackErrors;
+use JoelButcher\Socialstream\Contracts\OAuthProviderLinkFailedResponse;
use JoelButcher\Socialstream\Contracts\ResolvesSocialiteUsers;
use JoelButcher\Socialstream\Contracts\SocialstreamResponse;
+use JoelButcher\Socialstream\Events\OAuthProviderLinkFailed;
+use JoelButcher\Socialstream\Providers;
+use Laravel\Jetstream\Jetstream;
use Laravel\Socialite\Two\InvalidStateException;
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
@@ -58,4 +66,57 @@ public function callback(Request $request, string $provider): SocialstreamRespon
return $this->authenticator->authenticate($provider, $providerAccount);
}
+
+ /**
+ * Show the oauth confirmation page.
+ */
+ public function prompt(string $provider): View
+ {
+ return view('socialstream::oauth.prompt', [
+ 'provider' => $provider,
+ ]);
+ }
+
+ public function confirm(string $provider): SocialstreamResponse|RedirectResponse
+ {
+ $user = auth()->user();
+ $providerAccount = cache()->pull("socialstream.{$user->id}:$provider.provider");
+
+ $result = request()->input('result');
+
+ if ($result === 'deny') {
+ event(new OAuthProviderLinkFailed($user, $provider, null, $providerAccount));
+
+ $this->flashError(
+ __('Failed to link :provider account. User denied the request.', ['provider' => Providers::name($provider)]),
+ );
+
+ return app(OAuthProviderLinkFailedResponse::class);
+ }
+
+ if (!$providerAccount) {
+ throw new \DomainException(
+ message: 'Could not retrieve social provider information.'
+ );
+ }
+
+ return $this->authenticator->linkProvider($user, $provider, $providerAccount);
+ }
+
+ private function flashError(string $error): void
+ {
+ if (auth()->check()) {
+ if (class_exists(Jetstream::class)) {
+ Session::flash('flash.banner', $error);
+ Session::flash('flash.bannerStyle', 'danger');
+
+ return;
+ }
+ }
+
+ Session::flash('errors', (new ViewErrorBag())->put(
+ 'default',
+ new MessageBag(['socialstream' => $error])
+ ));
+ }
}
diff --git a/stubs/jetstream/app/Models/User.php b/stubs/jetstream/app/Models/User.php
index 4e118150..0222d954 100644
--- a/stubs/jetstream/app/Models/User.php
+++ b/stubs/jetstream/app/Models/User.php
@@ -68,7 +68,7 @@ class User extends Authenticatable
/**
* Get the URL to the user's profile photo.
*/
- public function profilePhotoUrl(): Attribute
+ protected function profilePhotoUrl(): Attribute
{
return filter_var($this->profile_photo_path, FILTER_VALIDATE_URL)
? Attribute::get(fn () => $this->profile_photo_path)
diff --git a/stubs/jetstream/app/Models/UserWithTeams.php b/stubs/jetstream/app/Models/UserWithTeams.php
index 27be9534..760bf8c5 100644
--- a/stubs/jetstream/app/Models/UserWithTeams.php
+++ b/stubs/jetstream/app/Models/UserWithTeams.php
@@ -70,7 +70,7 @@ class User extends Authenticatable
/**
* Get the URL to the user's profile photo.
*/
- public function profilePhotoUrl(): Attribute
+ protected function profilePhotoUrl(): Attribute
{
return filter_var($this->profile_photo_path, FILTER_VALIDATE_URL)
? Attribute::get(fn () => $this->profile_photo_path)