main 49cfcb8c7175 cached
68 files
134.6 KB
34.6k tokens
172 symbols
1 requests
Download .txt
Repository: DutchCodingCompany/filament-socialite
Branch: main
Commit: 49cfcb8c7175
Files: 68
Total size: 134.6 KB

Directory structure:
gitextract_wtq0hz3c/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── dependabot-auto-merge.yml
│       ├── php-cs-fixer.yml
│       ├── phpstan.yml
│       ├── run-tests.yml
│       └── update-changelog.yml
├── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── UPGRADE.md
├── bin/
│   └── upgrade-v2
├── composer.json
├── config/
│   └── filament-socialite.php
├── database/
│   └── migrations/
│       └── create_socialite_users_table.php.stub
├── package.json
├── phpstan-baseline.neon
├── phpstan.neon.dist
├── phpunit.xml
├── rector.php
├── resources/
│   ├── css/
│   │   └── plugin.css
│   ├── dist/
│   │   └── plugin.css
│   ├── lang/
│   │   └── en/
│   │       └── auth.php
│   └── views/
│       ├── .gitkeep
│       └── components/
│           └── buttons.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Events/
│   │   ├── InvalidState.php
│   │   ├── Login.php
│   │   ├── Registered.php
│   │   ├── RegistrationNotEnabled.php
│   │   ├── SocialiteUserConnected.php
│   │   └── UserNotAllowed.php
│   ├── Exceptions/
│   │   ├── GuardNotStateful.php
│   │   ├── ImplementationException.php
│   │   ├── InvalidCallbackPayload.php
│   │   └── ProviderNotConfigured.php
│   ├── FilamentSocialitePlugin.php
│   ├── FilamentSocialiteServiceProvider.php
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── SocialiteLoginController.php
│   │   └── Middleware/
│   │       └── PanelFromUrlQuery.php
│   ├── Models/
│   │   ├── Contracts/
│   │   │   └── FilamentSocialiteUser.php
│   │   └── SocialiteUser.php
│   ├── Provider.php
│   ├── Traits/
│   │   ├── Callbacks.php
│   │   ├── CanBeHidden.php
│   │   ├── Models.php
│   │   └── Routes.php
│   └── View/
│       └── Components/
│           └── Buttons.php
├── tailwind.config.js
└── tests/
    ├── Fixtures/
    │   ├── TestSocialiteUser.php
    │   ├── TestTeam.php
    │   ├── TestTenantUser.php
    │   ├── TestUser.php
    │   ├── change_nullable_password_on_users_table.php
    │   ├── create_socialite_users_table.php
    │   ├── create_team_user_table.php
    │   └── create_teams_table.php
    ├── SocialiteLoginAuthorizationTest.php
    ├── SocialiteLoginTest.php
    ├── SocialiteStatelessLoginTest.php
    ├── SocialiteTenantLoginTest.php
    └── TestCase.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2


================================================
FILE: .gitattributes
================================================
# Path-based git attributes
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html

# Ignore all test and documentation with "export-ignore".
/.github            export-ignore
/.gitattributes     export-ignore
/.gitignore         export-ignore
/phpunit.xml.dist   export-ignore
/art                export-ignore
/docs               export-ignore
/tests              export-ignore
/.editorconfig      export-ignore
/.php_cs.dist.php   export-ignore
/psalm.xml          export-ignore
/psalm.xml.dist     export-ignore
/testbench.yaml     export-ignore
/UPGRADING.md       export-ignore
/phpstan.neon.dist  export-ignore
/phpstan-baseline.neon  export-ignore


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug report
description: Report a problem you're experiencing
body:
  - type: markdown
    attributes:
      value: |
        Before opening a bug report, please search the existing issues (both open and closed).

        ---

        Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information.
  - type: input
    id: plugin-version
    attributes:
      label: Plugin Version
      description: Please provide the version of filament socialite installed in your project.
      placeholder: v2.0.0
    validations:
      required: true
  - type: input
    id: filament-version
    attributes:
      label: Filament Version
      description: Please provide the full filament version of your project.
      placeholder: v3.0.0
    validations:
      required: true
  - type: input
    id: laravel-version
    attributes:
      label: Laravel Version
      description: Please provide the full Laravel version of your project.
      placeholder: v10.0.0
    validations:
      required: true
  - type: input
    id: livewire-version
    attributes:
      label: Livewire Version
      description: Please provide the full Livewire version of your project, if applicable.
      placeholder: v3.0.0
  - type: input
    id: php-version
    attributes:
      label: PHP Version
      description: Please provide the full PHP version of your server.
      placeholder: PHP 8.3.0
    validations:
      required: true
  - type: textarea
    id: description
    attributes:
      label: Problem description
      description: What happened when you experienced the problem?
    validations:
      required: true
  - type: textarea
    id: expectation
    attributes:
      label: Expected behavior
      description: What did you expect to happen instead?
    validations:
      required: true
  - type: textarea
    id: steps
    attributes:
      label: Steps to reproduce
      description: Which steps do we need to take to reproduce the problem? Any code examples need to be **as short as possible**, remove any code that is unrelated to the bug.
    validations:
      required: true
  - type: textarea
    id: logs
    attributes:
      label: Relevant log output
      description: If applicable, provide relevant log output. No need for backticks here.
      render: shell


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Ask a question
    url: https://discord.com/channels/883083792112300104/962299008259342366
    about: Ask the community for help
  - name: Request a feature
    url: https://discord.com/channels/883083792112300104/962299008259342366
    about: Share ideas for new features
  - name: Report a security issue
    url: https://github.com/DutchCodingCompany/filament-socialite/security/policy
    about: Learn how to notify us for sensitive bugs


================================================
FILE: .github/dependabot.yml
================================================
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    labels:
      - "dependencies"


================================================
FILE: .github/workflows/dependabot-auto-merge.yml
================================================
name: dependabot-auto-merge
on: pull_request_target

permissions:
  pull-requests: write
  contents: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:

      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v3.0.0
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Auto-merge Dependabot PRs for semver-minor updates
        if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

      - name: Auto-merge Dependabot PRs for semver-patch updates
        if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .github/workflows/php-cs-fixer.yml
================================================
name: Check & fix styling

on: [push]

jobs:
  php-cs-fixer:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: ${{ github.head_ref }}

      - name: Run PHP CS Fixer
        uses: docker://oskarstark/php-cs-fixer-ga
        with:
          args: --config=.php-cs-fixer.php --allow-risky=yes

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v7
        with:
          commit_message: Fix styling


================================================
FILE: .github/workflows/phpstan.yml
================================================
name: PHPStan

on:
  push:
    paths:
      - '**.php'
      - .github/workflows/phpstan.yml
      - phpstan.neon.dist
  pull_request:

jobs:
  phpstan:
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest]
        php: [8.4, 8.3, 8.2]
        laravel: ['11.*', '12.*', '13.*']
        stability: [prefer-stable]
        include:
          - laravel: 11.*
            testbench: 9.*
          - laravel: 12.*
            testbench: 10.*
          - laravel: 13.*
            testbench: 11.*
        exclude:
          - laravel: 13.*
            php: 8.2

    name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
          coverage: none

      - name: Install dependencies
        run: |
          composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
          composer update --${{ matrix.stability }} --prefer-dist --no-interaction

      - name: List Installed Dependencies
        run: composer show -D

      - name: Run PHPStan
        run: vendor/bin/phpstan --error-format=github


================================================
FILE: .github/workflows/run-tests.yml
================================================
name: run-tests

on:
  push:
    paths:
      - '**.php'
      - .github/workflows/run-tests.yml
      - phpunit.xml.dist
      - composer.json
      - composer.lock
  pull_request:

jobs:
  test:
    runs-on: ${{ matrix.os }}

    timeout-minutes: 5

    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest]
        php: [8.4, 8.3, 8.2]
        laravel: ['11.*', '12.*', '13.*']
        stability: [prefer-stable]
        include:
          - laravel: 11.*
            testbench: 9.*
          - laravel: 12.*
            testbench: 10.*
          - laravel: 13.*
            testbench: 11.*
        exclude:
          - laravel: 13.*
            php: 8.2

    name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
          coverage: none

      - name: Setup problem matchers
        run: |
          echo "::add-matcher::${{ runner.tool_cache }}/php.json"
          echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

      - name: Install dependencies
        run: |
          composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
          composer update --${{ matrix.stability }} --prefer-dist --no-interaction

      - name: List Installed Dependencies
        run: composer show -D

      - name: Execute tests
        run: vendor/bin/phpunit tests


================================================
FILE: .github/workflows/update-changelog.yml
================================================
name: "Update Changelog"

on:
  release:
    types: [released]

jobs:
  update:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: main

      - name: Update Changelog
        uses: stefanzweifel/changelog-updater-action@v1
        with:
          latest-version: ${{ github.event.release.name }}
          release-notes: ${{ github.event.release.body }}

      - name: Commit updated CHANGELOG
        uses: stefanzweifel/git-auto-commit-action@v7
        with:
          branch: main
          commit_message: Update CHANGELOG
          file_pattern: CHANGELOG.md


================================================
FILE: .gitignore
================================================
.idea
.php_cs
.php_cs.cache
.phpunit.result.cache
build
composer.lock
package-lock.json
coverage
docs
phpstan.neon
testbench.yaml
vendor
node_modules
.php-cs-fixer.cache


================================================
FILE: .php-cs-fixer.php
================================================
<?php

use PhpCsFixer\Config;
use PhpCsFixer\Finder;

$rules = [
    'array_indentation' => true,
    'array_syntax' => ['syntax' => 'short'],
    'binary_operator_spaces' => [
        'default' => 'single_space',
        // 'operators' => ['=>' => null], // single space makes code look more coherent in style. But sometimes it is not beter, in that case, manually override.
    ],
    'blank_line_after_namespace' => true,
    'blank_line_after_opening_tag' => true,
    'blank_line_before_statement' => [
        'statements' => ['return'],
    ],
    'braces' => true,
    'cast_spaces' => true,
    'class_attributes_separation' => [
        'elements' => [
            'const' => 'only_if_meta',
            'method' => 'one',
            'property' => 'one',
            'trait_import' => 'none',
        ],
    ],
    'class_definition' => [
        'multi_line_extends_each_single_line' => true,
        'single_item_single_line' => true,
        'single_line' => true,
    ],
    'concat_space' => [
        'spacing' => 'none',
    ],
    'constant_case' => ['case' => 'lower'],
    'declare_equal_normalize' => true,
    'elseif' => true,
    'encoding' => true,
    'full_opening_tag' => true,
    'fully_qualified_strict_types' => false,
    // added by Shift
    'function_declaration' => true,
    'function_typehint_space' => true,
    'general_phpdoc_tag_rename' => true,
    'heredoc_to_nowdoc' => true,
    'include' => true,
    'increment_style' => ['style' => 'post'],
    'indentation_type' => true,
    'linebreak_after_opening_tag' => true,
    'line_ending' => true,
    'lowercase_cast' => true,
    'lowercase_keywords' => true,
    'lowercase_static_reference' => true,
    // added from Symfony
    'magic_method_casing' => true,
    // added from Symfony
    'magic_constant_casing' => true,
    'method_argument_space' => [
        'on_multiline' => 'ignore',
    ],
    'multiline_whitespace_before_semicolons' => [
        'strategy' => 'no_multi_line',
    ],
    'native_function_casing' => true,
    'no_alias_functions' => true,
    'no_extra_blank_lines' => [
        'tokens' => [
            'extra',
            'throw',
            'use',
            'switch',
            'case',
            'default',
        ],
    ],
    'no_blank_lines_after_class_opening' => true,
    'no_blank_lines_after_phpdoc' => true,
    'no_closing_tag' => true,
    'no_empty_phpdoc' => true,
    'no_empty_statement' => true,
    'no_leading_import_slash' => true,
    'no_leading_namespace_whitespace' => true,
    'no_mixed_echo_print' => [
        'use' => 'echo',
    ],
    'no_multiline_whitespace_around_double_arrow' => true,
    'no_short_bool_cast' => true,
    'no_singleline_whitespace_before_semicolons' => true,
    'no_spaces_after_function_name' => true,
    'no_spaces_around_offset' => [
        'positions' => [
            'inside',
            'outside',
        ],
    ],
    'no_spaces_inside_parenthesis' => true,
    'no_trailing_comma_in_list_call' => true,
    'no_trailing_comma_in_singleline_array' => true,
    'no_trailing_whitespace' => true,
    'no_trailing_whitespace_in_comment' => true,
    'no_unneeded_control_parentheses' => [
        'statements' => [
            'break',
            'clone',
            'continue',
            'echo_print',
            'return',
            'switch_case',
            'yield',
        ],
    ],
    'no_unreachable_default_argument_value' => true,
    'no_useless_return' => true,
    'no_whitespace_before_comma_in_array' => true,
    'no_whitespace_in_blank_line' => true,
    'normalize_index_brace' => true,
    'not_operator_with_successor_space' => true,
    'object_operator_without_whitespace' => true,
    'ordered_imports' => [
        'sort_algorithm' => 'alpha',
        'imports_order' => [
            'class',
            'function',
            'const',
        ],
    ],
    'psr_autoloading' => true,
    'phpdoc_indent' => true,
    'phpdoc_inline_tag_normalizer' => true,
    'phpdoc_no_access' => true,
    'phpdoc_no_package' => true,
    'phpdoc_no_useless_inheritdoc' => true,
    'phpdoc_scalar' => true,
    'phpdoc_single_line_var_spacing' => true,
    'phpdoc_summary' => false,
    'phpdoc_to_comment' => false,
    // override to preserve user preference
    'phpdoc_tag_type' => true,
    'phpdoc_trim' => true,
    'phpdoc_types' => true,
    'phpdoc_var_without_name' => true,
    'self_accessor' => true,
    'short_scalar_cast' => true,
    'simplified_null_return' => false,
    // disabled as "risky"
    'single_blank_line_at_eof' => true,
    'single_blank_line_before_namespace' => true,
    'single_class_element_per_statement' => [
        'elements' => [
            'const',
            'property',
        ],
    ],
    'single_import_per_statement' => true,
    'single_line_after_imports' => true,
    'single_line_comment_style' => [
        'comment_types' => ['hash'],
    ],
    'single_quote' => true,
    'space_after_semicolon' => true,
    'standardize_not_equals' => true,
    'switch_case_semicolon_to_colon' => true,
    'switch_case_space' => true,
    'ternary_operator_spaces' => true,
    'trailing_comma_in_multiline' => [
        'elements' => [
            'arrays',
            'parameters',
        ],
    ],
    'trim_array_spaces' => true,
    'types_spaces' => [
        'space' => 'single',
    ],
    'unary_operator_spaces' => true,
    'visibility_required' => [
        'elements' => [
            'method',
            'property',
            'const',
        ],
    ],
    'whitespace_after_comma_in_array' => true,

    // DCC
    'align_multiline_comment' => ['comment_type' => 'phpdocs_like'],
    'simplified_if_return' => true,
    'method_chaining_indentation' => true,
];

$finder = Finder::create()
    ->in([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->name('*.php')
    ->notName('*.blade.php')
    ->ignoreDotFiles(true)
    ->ignoreVCS(true);

return (new Config)
    ->setFinder($finder)
    ->setRules($rules)
    ->setRiskyAllowed(true)
    ->setUsingCache(true);


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to `filament-socialite` will be documented in this file.

## [3.2.1 - 2026-04-14](https://github.com/DutchCodingCompany/filament-socialite/compare/3.2.0...3.2.1)

## What's Changed
* fix: update state retrieval method in PanelFromUrlQuery middleware by @fului in https://github.com/DutchCodingCompany/filament-socialite/pull/153
* Bump dependabot/fetch-metadata from 2.5.0 to 3.0.0 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/151

## [3.2.0 - 2026-03-19](https://github.com/DutchCodingCompany/filament-socialite/compare/3.1.0...3.2.0)

## What's Changed
* Add return type declarations to migration methods by @phh in https://github.com/DutchCodingCompany/filament-socialite/pull/149
* Support laravel 13 https://github.com/DutchCodingCompany/filament-socialite/pull/150

## [3.1.0 - 2026-01-19](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.1...3.1.0)

## What's Changed
- Add support for filament v5 by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/147
- Bump dependabot/fetch-metadata from 2.4.0 to 2.5.0 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/145

## [3.0.1 - 2025-12-16](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.0...3.0.1)

## What's Changed
- Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/135
- Bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/139
- Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/DutchCodingCompany/filament-socialite/pull/141
- Add foreign key constraint with cascade behaviour. by @chrillep in https://github.com/DutchCodingCompany/filament-socialite/pull/143
  - **NOTE**: this will not change existing `socialite_users` tables. Please **consider adding** the constraint in a separate migration yourself 

## [3.0.0 - 2025-08-11](https://github.com/DutchCodingCompany/filament-socialite/compare/2.4.0...3.0.0)
- Tag major version
- BREAKING CHANGE: Implement fix for slug issue https://github.com/DutchCodingCompany/filament-socialite/issues/127  
  The package now uses `path` instead of `id` as default prefix as it should have done. In order to revert to previous behaviour, use slug to override the behaviour:
  ```php
  ->plugin(
      FilamentSocialitePlugin::make()
          ->slug('admin') // change this to the panel's ID
          // other config for plugin
  )
  ```

## [3.0.0-beta3 - 2025-07-18](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.0-beta2...3.0.0-beta3)

## What's Changed
* Include compiled styles
  
## [3.0.0-beta2 - 2025-06-23](https://github.com/DutchCodingCompany/filament-socialite/compare/3.0.0-alpha1...3.0.0-beta2)

## What's Changed
* BREAKING CHANGE: Implement fix for slug issue https://github.com/DutchCodingCompany/filament-socialite/issues/127  
  The package now uses `path` instead of `id` as default prefix as it should have done. In order to revert to previous behaviour, use slug to override the behaviour:
  ```php
  ->plugin(
      FilamentSocialitePlugin::make()
          ->slug('admin') // change this to the panel's ID
          // other config for plugin
  )
  ```

## [3.0.0-alpha1 - 2025-06-05](https://github.com/DutchCodingCompany/filament-socialite/compare/2.4.0...3.0.0-alpha1) / [3.0.0-beta1 - 2025-06-05](https://github.com/DutchCodingCompany/filament-socialite/compare/2.4.0...3.0.0-beta1)

## What's Changed
* Filament V4 support by @erikgaal in https://github.com/DutchCodingCompany/filament-socialite/pull/131

## [2.4.0 - 2025-02-25](https://github.com/DutchCodingCompany/filament-socialite/compare/2.3.1...2.4.0)

## What's Changed
* Laravel 12.x Compatibility by @laravel-shift in https://github.com/DutchCodingCompany/filament-socialite/pull/125

## [2.3.1 - 2025-02-06](https://github.com/DutchCodingCompany/filament-socialite/compare/2.3.0...2.3.1)

## What's Changed
* Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/123
* Add data to events by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/124

## [2.3.0 - 2024-11-29](https://github.com/DutchCodingCompany/filament-socialite/compare/2.2.1...2.3.0)

## What's Changed
* Add option to hide providers by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/122
* Add support for php 8.4

## [2.2.1 - 2024-07-17](https://github.com/DutchCodingCompany/filament-socialite/compare/2.2.0...2.2.1)

## What's Changed
* Revert model property changes by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/110

## [2.2.0 - 2024-07-15](https://github.com/DutchCodingCompany/filament-socialite/compare/2.1.1...2.2.0)

## What's Changed
* Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/106
* Add new callback route for stateless OAuth flows by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/105

## [2.1.1 - 2024-06-21](https://github.com/DutchCodingCompany/filament-socialite/compare/2.1.0...2.1.1)

## What's Changed
* Improve Socialite driver typings + callable typings by @juliangums in https://github.com/DutchCodingCompany/filament-socialite/pull/103

## New Contributors
* @juliangums made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/103

## [2.1.0 - 2024-06-21](https://github.com/DutchCodingCompany/filament-socialite/compare/2.0.0...2.1.0)

* Add Authorization Callback by @petecoop in https://github.com/DutchCodingCompany/filament-socialite/pull/100

## [2.0.0 - 2024-06-04](https://github.com/DutchCodingCompany/filament-socialite/compare/1.5.0...2.0.0)
* **Please check the revised [README.md](https://github.com/DutchCodingCompany/filament-socialite/blob/main/README.md) and [UPGRADE.md](https://github.com/DutchCodingCompany/filament-socialite/blob/main/UPGRADE.md)! Many functions have been renamed.**
* Refactor package for better consistency with Filament code standards https://github.com/DutchCodingCompany/filament-socialite/pull/90

## [1.5.0 - 2024-06-04](https://github.com/DutchCodingCompany/filament-socialite/compare/1.4.1...1.5.0)

* Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/89
* Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/91
* Compatible with Stateless Authentication by @LittleHans8 in https://github.com/DutchCodingCompany/filament-socialite/pull/96

## [1.4.1 - 2024-03-20](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.4.0...1.4.1)
* Provide oauth user to login event by @dcc-bjorn in https://github.com/DutchCodingCompany/filament-socialite/pull/88

## [1.4.0 - 2024-03-12](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.3.1...1.4.0)
* Laravel 11 support by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/87

## [1.3.1 - 2024-03-05](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.3.0...1.3.1)
* Add $provider as required by the callback by @phh in https://github.com/DutchCodingCompany/filament-socialite/pull/83
* Never use SPA mode for oauth links + spacing by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/85

## [1.3.0 - 2024-03-01](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.2.0...1.3.0)
* Update CHANGELOG.md by @bramr94 in https://github.com/DutchCodingCompany/filament-socialite/pull/70
* Add socialite test by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/78
* Improve actions by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/79
* Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/51
* feature: allow socialite user model customization by @kykurniawan in https://github.com/DutchCodingCompany/filament-socialite/pull/72
* Add registration enabled callable by @bert-w in https://github.com/DutchCodingCompany/filament-socialite/pull/80
* Multi-tenancy support by @bramr94 in https://github.com/DutchCodingCompany/filament-socialite/pull/76

## [1.2.0 - 2024-01-31](https://github.com/DutchCodingCompany/filament-socialite/compare/v1.1.1...1.2.0)
- Add option to add optional parameters in https://github.com/DutchCodingCompany/filament-socialite/pull/69

## [1.1.1 - 2024-01-18](https://github.com/DutchCodingCompany/filament-socialite/compare/1.1.0...1.1.1)
- Improve domain routing in https://github.com/DutchCodingCompany/filament-socialite/pull/61
- Update README in https://github.com/DutchCodingCompany/filament-socialite/pull/64

## [1.1.0 - 2024-01-08](https://github.com/DutchCodingCompany/filament-socialite/compare/1.0.1...1.1.0)
- Add button customization options in https://github.com/DutchCodingCompany/filament-socialite/pull/59

## [1.0.1 - 2023-12-18](https://github.com/DutchCodingCompany/filament-socialite/compare/1.0.0...1.0.1)
- Resolve plugin registration issue [#54](https://github.com/DutchCodingCompany/filament-socialite/issues/54)

## [1.0.0 - 2023-12-05](https://github.com/DutchCodingCompany/filament-socialite/compare/0.2.2...1.0.0)
- Added support for Filament v3 through the plugin setup
- Added support for multiple panels
- See [UPGRADE.md](UPGRADE.md)

## 0.2.2 - 2022-06-14

### What's Changed

- Fix readme by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/15
- use Filament-fortify render hook by @wychoong in https://github.com/DutchCodingCompany/filament-socialite/pull/16

### New Contributors

- @wychoong made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/16

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.2.1...0.2.2

## 0.2.1 - 2022-05-25

## What's Changed

- Fix user model instantiating by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/14

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.2.0...0.2.1

## 0.2.0 - 2022-05-24

## Breaking changes

- `Events\DomainFailed` renamed to `Events\UserNotAllowed`
- `Events\RegistrationFailed` renamed to `Events\RegistrationNotEnabled`

## What's Changed

- Refactor the controller for extendability and customization by @dododedodonl in https://github.com/DutchCodingCompany/filament-socialite/pull/13

## New Contributors

- @dododedodonl made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/13

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.5...0.2.0

## 0.1.5 - 2022-05-20

## What's Changed

- Fix missing variable for registered event by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/11

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.4...0.1.5

## 0.1.4 - 2022-05-06

## What's Changed

- Feature: Adds buttons blade component by @oyepez003 in https://github.com/DutchCodingCompany/filament-socialite/pull/8
- Feature: Add login events dispatching by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/5

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.3...0.1.4

## 0.1.3 - 2022-05-04

## What's Changed

- Bugfix: Avoid returning 403 when a user exists based on the oauth-email . by @oyepez003 in https://github.com/DutchCodingCompany/filament-socialite/pull/7

## New Contributors

- @oyepez003 made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/7

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.2...0.1.3

## 0.1.2 - 2022-05-03

## What's Changed

- Bump dependabot/fetch-metadata from 1.3.0 to 1.3.1 by @dependabot in https://github.com/DutchCodingCompany/filament-socialite/pull/2
- Add Laravel 8 support and make fontawesome icons optional by @marcoboers in https://github.com/DutchCodingCompany/filament-socialite/pull/4

## New Contributors

- @marcoboers made their first contribution in https://github.com/DutchCodingCompany/filament-socialite/pull/4

**Full Changelog**: https://github.com/DutchCodingCompany/filament-socialite/compare/0.1.1...0.1.2

## 0.1.1 - 2022-04-11

## What's Changed

- Fix registration flow

## 0.1.0 - 2022-04-08

### Initial Release

- Add social login links to login page
- Support Socialite OAuth flow
- Support registration flow
- Support domain allowlist for internal use
- Dark mode support
- Blade Font Awesome brand icons


================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)

Copyright (c) DutchCodingCompany <m@rcoboe.rs>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://banners.beyondco.de/Filament%20Socialite.png?theme=dark&packageManager=composer+require&packageName=DutchCodingCompany%2Ffilament-socialite&pattern=architect&style=style_1&description=Add+OAuth+login+through+Laravel+Socialite+to+Filament.&md=1&showWatermark=0&fontSize=100px&images=user-group">
  <img src="https://banners.beyondco.de/Filament%20Socialite.png?theme=light&packageManager=composer+require&packageName=DutchCodingCompany%2Ffilament-socialite&pattern=architect&style=style_1&description=Add+OAuth+login+through+Laravel+Socialite+to+Filament.&md=1&showWatermark=0&fontSize=100px&images=user-group">
</picture>

# Social login for Filament through Laravel Socialite

[![Latest Version on Packagist](https://img.shields.io/packagist/v/dutchcodingcompany/filament-socialite.svg?style=flat-square)](https://packagist.org/packages/dutchcodingcompany/filament-socialite)
[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/dutchcodingcompany/filament-socialite/run-tests?label=tests)](https://github.com/dutchcodingcompany/filament-socialite/actions?query=workflow%3Arun-tests+branch%3Amain)
[![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/dutchcodingcompany/filament-socialite/Check%20&%20fix%20styling?label=code%20style)](https://github.com/dutchcodingcompany/filament-socialite/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain)
[![Total Downloads](https://img.shields.io/packagist/dt/dutchcodingcompany/filament-socialite.svg?style=flat-square)](https://packagist.org/packages/dutchcodingcompany/filament-socialite)

Add OAuth2 login through Laravel Socialite to Filament. OAuth1 (eg. Twitter) is not supported at this time.

## Installation

| Filament version                                                                                                                                               | Package version | Readme                                                                               |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|--------------------------------------------------------------------------------------|
| [^5.0.0](https://github.com/filamentphp/filament/tree/5.x)                                                                              | ^3.1           | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/main/README.md) |
| [^4.0.0](https://github.com/filamentphp/filament/tree/4.x)                                                                              | 3.x.x           | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/main/README.md) |
| [^3.2.44](https://github.com/filamentphp/filament/releases/tag/v3.2.44) (if using [SPA mode](https://filamentphp.com/docs/3.x/panels/configuration#spa-mode))  | 2.x.x           | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/2.x/README.md) |
| [^3.2.44](https://github.com/filamentphp/filament/releases/tag/v3.2.44) (if using [SPA mode](https://filamentphp.com/docs/3.x/panels/configuration#spa-mode))  | ^1.3.1          |                                                                                      |
| 3.x                                                                                                                                                            | 1.x.x           | [Link](https://github.com/DutchCodingCompany/filament-socialite/blob/1.x/README.md)  |
| 2.x                                                                                                                                                            | 0.x.x           |                                                                                      |

Install the package via composer:

```bash
composer require dutchcodingcompany/filament-socialite
```

Publish and migrate the migration file:

```bash
php artisan vendor:publish --tag="filament-socialite-migrations"
php artisan migrate
```

Other configuration files include:
```bash
php artisan vendor:publish --tag="filament-socialite-config"
php artisan vendor:publish --tag="filament-socialite-views"
php artisan vendor:publish --tag="filament-socialite-translations"
```

You need to register the plugin in the Filament panel provider (the default filename is `app/Providers/Filament/AdminPanelProvider.php`). The following options are available:

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;
use Filament\Support\Colors;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
use Illuminate\Contracts\Auth\Authenticatable;

// ...
->plugin(
    FilamentSocialitePlugin::make()
        // (required) Add providers corresponding with providers in `config/services.php`. 
        ->providers([
            // Create a provider 'gitlab' corresponding to the Socialite driver with the same name.
            Provider::make('gitlab')
                ->label('GitLab')
                ->icon('fab-gitlab')
                ->color(Color::hex('#2f2a6b'))
                ->outlined(false)
                ->stateless(false)
                ->scopes(['...'])
                ->with(['...']),
        ])
        // (optional) Override the panel slug to be used in the oauth routes. Defaults to the panel's configured path.
        ->slug('admin')
        // (optional) Enable/disable registration of new (socialite-) users.
        ->registration(true)
        // (optional) Enable/disable registration of new (socialite-) users using a callback.
        // In this example, a login flow can only continue if there exists a user (Authenticatable) already.
        ->registration(fn (string $provider, SocialiteUserContract $oauthUser, ?Authenticatable $user) => (bool) $user)
        // (optional) Change the associated model class.
        ->userModelClass(\App\Models\User::class)
        // (optional) Change the associated socialite class (see below).
        ->socialiteUserModelClass(\App\Models\SocialiteUser::class)
);
```

This package automatically adds 2 routes per panel to make the OAuth flow possible: a redirector and a callback. When
setting up your **external OAuth app configuration**, enter the following callback URL (in this case for the Filament
panel with ID `admin` and the `github` provider):
```
https://example.com/admin/oauth/callback/github
```

A multi-panel callback route is available as well that does not contain the panel ID in the url. Instead, it determines
the panel ID from an encrypted `state` input (`...?state=abcd1234`). This allows you to create a single OAuth
application for multiple Filament panels that use the same callback URL. Note that this only works for _stateful_ OAuth
apps:

```
https://example.com/oauth/callback/github
```

If in doubt, run `php artisan route:list` to see which routes are available to you.

### Icons

You can specify a custom icon for each of your login providers. You can add Font Awesome brand
icons made available through [Blade Font Awesome](https://github.com/owenvoke/blade-fontawesome) by running:
```bash
composer require owenvoke/blade-fontawesome
```

### Registration flow

This package supports account creation for users. However, to support this flow it is important that the `password`
attribute on your `User` model is nullable. For example, by adding the following to your users table migration.
Or you could opt for customizing the user creation, see below.

```php
$table->string('password')->nullable();
```

### Domain Allow list

This package supports the option to limit the users that can login with the OAuth login to users of a certain domain.
This can be used to setup SSO for internal use.

```php
->plugin(
    FilamentSocialitePlugin::make()
        // ...
        ->registration(true)
        ->domainAllowList(['localhost'])
);
```

### Changing how an Authenticatable user is created or retrieved

You can use the `createUserUsing` and `resolveUserUsing` methods to change how a user is created or retrieved.

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

->plugin(
    FilamentSocialitePlugin::make()
        // ...
        ->createUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) {
            // Logic to create a new user.
        })
        ->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) {
            // Logic to retrieve an existing user.
        })
        ...
);
```

### Change how a Socialite user is created or retrieved

In your plugin options in your Filament panel, add the following method:

```php
// app/Providers/Filament/AdminPanelProvider.php
->plugins([
    FilamentSocialitePlugin::make()
        // ...
        ->socialiteUserModelClass(\App\Models\SocialiteUser::class)
```

This class should at the minimum implement the [`FilamentSocialiteUser`](/src/Models/Contracts/FilamentSocialiteUser.php) interface, like so:

```php
namespace App\Models;

use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

class SocialiteUser implements FilamentSocialiteUserContract
{
    public function getUser(): Authenticatable
    {
        //
    }

    public static function findForProvider(string $provider, SocialiteUserContract $oauthUser): ?self
    {
        //
    }
    
    public static function createForProvider(
        string $provider,
        SocialiteUserContract $oauthUser,
        Authenticatable $user
    ): self {
        //
    }
}
```

### Check if the user is authorized to use the application

You can use the `authorizeUserUsing` method to check if the user is authorized to use the application. **Note:** by [default](/src/Traits/Callbacks.php#L145) this method check if the user's email domain is in the domain allow list.

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

->plugin(
    FilamentSocialitePlugin::make()
        // ...
        ->authorizeUserUsing(function (FilamentSocialitePlugin $plugin, SocialiteUserContract $oauthUser) {
            // Logic to authorize the user.
            return FilamentSocialitePlugin::checkDomainAllowList($plugin, $oauthUser);
        })
        // ...
);
```

### Change login redirect

When your panel has [multi-tenancy](https://filamentphp.com/docs/4.x/users/tenancy) enabled, after logging in, the user will be redirected to their [default tenant](https://filamentphp.com/docs/4.x/users/tenancy#setting-the-default-tenant).
If you want to change this behavior, you can call the 'redirectAfterLoginUsing' method on the `FilamentSocialitePlugin`.

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser;

FilamentSocialitePlugin::make()
    ->redirectAfterLoginUsing(function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) {
        // Change the redirect behaviour here.
    });
```

## Events

There are a few events dispatched during the authentication process:

* `InvalidState(InvalidStateException $exception)`: When trying to retrieve the oauth (socialite) user, an invalid state was encountered
* `Login(FilamentSocialiteUserContract $socialiteUser)`: When a user successfully logs in
* `Registered(string $provider, SocialiteUserContract $oauthUser, FilamentSocialiteUserContract $socialiteUser)`: When a user and socialite user is successfully registered and logged in (when enabled in config)
* `RegistrationNotEnabled(string $provider, SocialiteUserContract $oauthUser, ?Auhthenticatable $user)`: When a user tries to login with an unknown account and registration is not enabled
* `SocialiteUserConnected(string $provider, SocialiteUserContract $oauthUser, FilamentSocialiteUserContract $socialiteUser)`: When a socialite user is created for an existing user
* `UserNotAllowed(SocialiteUserContract $oauthUser)`: When a user tries to login with an email which domain is not on the allowlist

## Scopes

Scopes can be added to the provider on the panel, for example:

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;

FilamentSocialitePlugin::make()
    ->providers([
        Provider::make('github')
            ->label('Github')
            ->icon('fab-github')
            ->scopes([
                // Add scopes here.
                'read:user',
                'public_repo',
            ]),
    ]),
```

## Optional parameters

You can add [optional parameters](https://laravel.com/docs/10.x/socialite#optional-parameters) to the request by adding a `with` key to the provider on the panel, for example:

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;

FilamentSocialitePlugin::make()
    ->providers([
        Provider::make('github')
            ->label('Github')
            ->icon('fab-github')
            ->with([
                // Add scopes here.
                // Add optional parameters here.
                'hd' => 'example.com',
            ]),
    ]),
```
## Visibility

You can set the visibility of a provider, if it is not visible, buttons will not be rendered. All functionality will still be enabled.

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;

FilamentSocialitePlugin::make()
    ->providers([
        Provider::make('github')
            ->visible(fn () => true),
    ]),
```

## Stateless Authentication
You can add `stateless` parameters to the provider configuration in the config/services.php config file, for example:

```php
'apple' => [
    'client_id' => '...',
    'client_secret' => '...',
    'stateless'=>true,
]
```

**Note:** you cannot use the `state` parameter, as it is used to determine from which Filament panel the user came from.

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Contributing

Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.

## Security Vulnerabilities

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

## Credits
- [All Contributors](../../contributors)

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

Use this section to tell people about which versions of your project are
currently being supported with security updates.

| Version | Supported          |
| ------- | ------------------ |
| 3.x   | :white_check_mark: |
| 2.x   | :warning: (security fixes only) |
| 1.x   | :no_entry_sign: |
| 0.x   | :no_entry_sign: |

## Reporting a Vulnerability

If you discover a security vulnerability within this plugin, please email Dutch Coding Company via [server@dutchcodingcompany.com](mailto:server@dutchcodingcompany.com). 
All security vulnerabilities will be promptly addressed.


================================================
FILE: UPGRADE.md
================================================
# Upgrade Guide
## `2.x.x` to `3.x.x` (Filament v4.x)

### Slug
In v3 of the plugin, the panel's configured path is used instead of it's ID when generating the callback URLs.  
In order to revert to previous behaviour, use slug to override the behaviour:
  ```php
  ->plugin(
      FilamentSocialitePlugin::make()
          ->slug('admin') // change this to the panel's ID
          // other config for plugin
  )
  ```

## `1.x.x` to `2.x.x` (Filament v3.x)

For version 2 we refactored most of the plugin to be more consistent with the Filament naming conventions. We've also moved some of the callbacks to the plugin, so they are configurable per panel.

### Method names

Every method name has been changed to be more consistent with the Filament naming conventions. The following changes have been made:

- `setProviders()` -> `providers()` 
- `setSlug()` -> `slug()` 
- `setLoginRouteName()` -> `loginRouteName()` 
- `setDashboardRouteName()` -> `dashboardRouteName()` 
- `setRememberLogin()` -> `rememberLogin()` 
- `setRegistrationEnabled()` -> `registration()` 
- `getRegistrationEnabled()` -> `getRegistration()` 
- `setDomainAllowList()` -> `domainAllowList()` 
- `setSocialiteUserModelClass()` -> `socialiteUserModelClass()`
- `setUserModelClass()` -> `userModelClass()` 
- `setShowDivider()` -> `showDivider()` 

**Note:** We've included a simple rector script which automatically updates the method names. It checks all panel providers in the `app\Provider\Filament` directory. You can run the script by executing the following command:

```bash
vendor/bin/upgrade-v2
```
#### Callbacks

**setCreateUserCallback()**

The `setCreateUserCallback()` has been renamed to `createUserUsing()`. This function was first registered in the `boot` method of your `AppServiceProvider.php`, but now it should be called on the plugin.

```php
FilamentSocialitePlugin::make()
    // ...
    ->createUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) {
        // Logic to create a new user.
    })
```

**setUserResolver()**

The `setUserResolver()` has been renamed to `resolveUserUsing()`. This function was first registered in the `boot` method of your `AppServiceProvider.php`, but now it should be called on the plugin.

```php
FilamentSocialitePlugin::make()
    // ...
    ->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) {
        // Logic to retrieve an existing user.
    })
```

**setLoginRedirectCallback()**

The `setLoginRedirectCallback()` has been renamed to `redirectAfterLoginUsing()`. This function was first registered in the `boot` method of your `AppServiceProvider.php`, but now it should be called on the plugin.

```php
FilamentSocialitePlugin::make()
    // ...
    ->redirectAfterLoginUsing(function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) {
        // Change the redirect behaviour here.
    })
```

#### Removals

**getOptionalParameters()**

This function was used internally only inside the `SocialiteLoginController`. If you haven't extended this controller, you can ignore this change.

Provider details can now be retrieved using `$plugin->getProvider($provider)->getWith()`.

**getProviderScopes()**

This function was used internally only inside the `SocialiteLoginController`. If you haven't extended this controller, you can ignore this change.

Provider details can now be retrieved using `$plugin->getProvider($provider)->getScopes()`.

### Configuration

**Providers**

Previously, providers were configured by passing a plain array. In the new setup, they should be created using the `Provider` class. The key should be passed as part of the `make()` function.

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;

FilamentSocialitePlugin::make()
    ->providers([
        Provider::make('gitlab')
            ->label('GitLab')
            ->icon('fab-gitlab')
            ->color(Color::hex('#2f2a6b')),
    ]),
```

**Scopes and Optional parameters**

Scopes and additional parameters for Socialite providers were previously configured in the `services.php` file, but have now been moved to the `->providers()` method on the Filament plugin.

```php
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;

FilamentSocialitePlugin::make()
    ->providers([
        Provider::make('gitlab')
            // ...
            ->scopes([
                // Add scopes here.
                'read:user',
                'public_repo',
            ]),
            ->with([
                // Add optional parameters here.
                 'hd' => 'example.com',
            ]),
    ]),
```

## `0.x.x` to `1.x.x` (Filament v3.x)
- Replace/republish the configuration file:
  - `sail artisan vendor:publish --provider="DutchCodingCompany\FilamentSocialite\FilamentSocialiteServiceProvider"`
- Update your panel configuration `App\Providers\Filament\YourPanelProvider` to include the plugin:
  - Append `->plugins([\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin::make()])`
  - Configure any options by chaining functions on the plugin.

## `0.x.x` (Filament v2.x)
- Initial version


================================================
FILE: bin/upgrade-v2
================================================
#!/usr/bin/env php
<?php

namespace Composer;

exec("vendor/bin/rector process --config vendor/dutchcodingcompany/filament-socialite/rector.php --clear-cache");


================================================
FILE: composer.json
================================================
{
    "name": "dutchcodingcompany/filament-socialite",
    "description": "Social login for Filament through Laravel Socialite",
    "keywords": [
        "DutchCodingCompany",
        "laravel",
        "filament-socialite"
    ],
    "homepage": "https://github.com/dutchcodingcompany/filament-socialite",
    "license": "MIT",
    "authors": [
        {
            "name": "Marco Boers",
            "email": "m@rcoboe.rs",
            "role": "Developer"
        },
        {
            "name": "Tom Janssen",
            "email": "dododedodonl@thor.edu",
            "role": "Developer"
        },
        {
            "name": "Bram Raaijmakers",
            "email": "bram@dutchcodingcompany.com",
            "role": "Developer"
        }
    ],
    "require": {
        "php": "^8.2",
        "filament/filament": "^4.0|^5.0",
        "illuminate/contracts": "^11.0|^12.0|^13.0",
        "laravel/socialite": "^5.5",
        "spatie/laravel-package-tools": "^1.9.2"
    },
    "require-dev": {
        "friendsofphp/php-cs-fixer": "^3.8",
        "larastan/larastan": "^2.9|^3.0",
        "nunomaduro/collision": "^7.0|^8.1",
        "orchestra/testbench": "^9.0|^10.0|^11.0",
        "phpunit/phpunit": "^10.0|^11.5.3|^12.5.12",
        "rector/rector": "^0.19.8|^2.0"
    },
    "suggest": {
        "owenvoke/blade-fontawesome": "^2.0"
    },
    "autoload": {
        "psr-4": {
            "DutchCodingCompany\\FilamentSocialite\\": "src",
            "DutchCodingCompany\\FilamentSocialite\\Tests\\": "tests",
            "DutchCodingCompany\\FilamentSocialite\\Database\\Factories\\": "database/factories",
            "Utils\\Rector\\": "utils/rector/src"
        }
    },
    "scripts": {
        "analyse": "vendor/bin/phpstan analyse",
        "test": "vendor/bin/phpunit"
    },
    "bin": [
        "bin/upgrade-v2"
    ],
    "config": {
        "sort-packages": true,
        "allow-plugins": {
            "phpstan/extension-installer": true
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialiteServiceProvider"
            ],
            "aliases": {
                "FilamentSocialite": "DutchCodingCompany\\FilamentSocialite\\Facades\\FilamentSocialite"
            }
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "autoload-dev": {
        "psr-4": {
            "DutchCodingCompany\\FilamentSocialite\\Tests\\": "tests"
        }
    }
}


================================================
FILE: config/filament-socialite.php
================================================
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | OAuth callback middleware
    |--------------------------------------------------------------------------
    |
    | This option defines the middleware that is applied to the OAuth callback url.
    |
    */

    'middleware' => [
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    ],
];


================================================
FILE: database/migrations/create_socialite_users_table.php.stub
================================================
<?php

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

return new class extends Migration {
    public function up(): void
    {
        Schema::create('socialite_users', function (Blueprint $table) {
            $table->id();

            $table->foreignId('user_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
            $table->string('provider');
            $table->string('provider_id');

            $table->timestamps();

            $table->unique([
                'provider',
                'provider_id',
            ]);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('socialite_users');
    }
};


================================================
FILE: package.json
================================================
{
    "private": true,
    "name": "filament-socialite",
    "devDependencies": {
        "@tailwindcss/cli": "^4.1.10",
        "@tailwindcss/postcss": "^4.1.10",
        "tailwindcss": "^4.1.10"
    },
    "scripts": {
        "dev": "npx @tailwindcss/cli -i ./resources/css/plugin.css -o ./resources/dist/plugin.css --watch",
        "watch": "npx @tailwindcss/cli -i ./resources/css/plugin.css -o ./resources/dist/plugin.css --watch",
        "prod": "npx @tailwindcss/cli -i ./resources/css/plugin.css -o ./resources/dist/plugin.css --minify"
    }
}


================================================
FILE: phpstan-baseline.neon
================================================
parameters:
	ignoreErrors:
		-
			message: '#^Class App\\Models\\User not found\.$#'
			identifier: class.notFound
			count: 1
			path: src/FilamentSocialitePlugin.php

		-
			message: '#^Property DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin\:\:\$userModelClass \(class\-string\<Illuminate\\Contracts\\Auth\\Authenticatable\>\) does not accept default value of type string\.$#'
			identifier: property.defaultValue
			count: 1
			path: src/FilamentSocialitePlugin.php

		-
			message: '#^Call to function method_exists\(\) with ''Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\VerifyCsrfToken'' and ''except'' will always evaluate to true\.$#'
			identifier: function.alreadyNarrowedType
			count: 1
			path: src/FilamentSocialiteServiceProvider.php

		-
			message: '#^Parameter \#1 \$value of method DutchCodingCompany\\FilamentSocialite\\Http\\Controllers\\SocialiteLoginController\:\:evaluate\(\) expects bool\|\(callable\(\)\: bool\), bool\|\(Closure\(string, Laravel\\Socialite\\Contracts\\User, Illuminate\\Contracts\\Auth\\Authenticatable\|null\)\: bool\) given\.$#'
			identifier: argument.type
			count: 1
			path: src/Http/Controllers/SocialiteLoginController.php

		-
			message: '#^Method DutchCodingCompany\\FilamentSocialite\\Models\\SocialiteUser\:\:user\(\) should return Illuminate\\Database\\Eloquent\\Relations\\BelongsTo\<Illuminate\\Database\\Eloquent\\Model, \$this\(DutchCodingCompany\\FilamentSocialite\\Models\\SocialiteUser\)\> but returns Illuminate\\Database\\Eloquent\\Relations\\BelongsTo\<Illuminate\\Contracts\\Auth\\Authenticatable&Illuminate\\Database\\Eloquent\\Model, \$this\(DutchCodingCompany\\FilamentSocialite\\Models\\SocialiteUser\)\>\.$#'
			identifier: return.type
			count: 1
			path: src/Models/SocialiteUser.php

		-
			message: '#^Parameter \#1 \$view of function view expects view\-string\|null, string given\.$#'
			identifier: argument.type
			count: 1
			path: src/View/Components/Buttons.php

		-
			message: '#^PHPDoc tag @property for property DutchCodingCompany\\FilamentSocialite\\Tests\\Fixtures\\TestTenantUser\:\:\$teams contains generic class Illuminate\\Support\\Collection but does not specify its types\: TKey, TValue$#'
			identifier: missingType.generics
			count: 1
			path: tests/Fixtures/TestTenantUser.php

		-
			message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:andReturn\(\)\.$#'
			identifier: method.notFound
			count: 3
			path: tests/TestCase.php

		-
			message: '#^Parameter \#1 \$callback of static method Illuminate\\Database\\Eloquent\\Factories\\Factory\<Illuminate\\Database\\Eloquent\\Model\>\:\:guessFactoryNamesUsing\(\) expects callable\(class\-string\<Illuminate\\Database\\Eloquent\\Model\>\)\: class\-string\<Illuminate\\Database\\Eloquent\\Factories\\Factory\>, Closure\(string\)\: non\-falsy\-string given\.$#'
			identifier: argument.type
			count: 1
			path: tests/TestCase.php


================================================
FILE: phpstan.neon.dist
================================================
includes:
    - ./vendor/larastan/larastan/extension.neon
    - ./phpstan-baseline.neon

parameters:
    level: 8

    paths:
        - config
        - database
        - src
        - tests


================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Test">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value="testing"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>


================================================
FILE: rector.php
================================================
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Utils\Rector\Rector\RenameMethods;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        'app/Providers/Filament/',
    ]);

    $rectorConfig->ruleWithConfiguration(
        \Rector\Renaming\Rector\MethodCall\RenameMethodRector::class,
        [
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setProviders",
                "providers",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setRegistrationEnabled",
                "registration",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setSlug",
                "slug",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setLoginRouteName",
                "loginRouteName",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setDashboardRouteName",
                "dashboardRouteName",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setRememberLogin",
                "rememberLogin",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setSocialiteUserModelClass",
                "socialiteUserModelClass",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setDomainAllowList",
                "domainAllowList",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setUserModelClass",
                "userModelClass",
            ),
            new \Rector\Renaming\ValueObject\MethodCallRename(
                "DutchCodingCompany\\FilamentSocialite\\FilamentSocialitePlugin",
                "setShowDivider",
                "showDivider",
            ),
        ]
    );
};


================================================
FILE: resources/css/plugin.css
================================================
@import "tailwindcss";

@config '../../tailwind.config.js';


================================================
FILE: resources/dist/plugin.css
================================================
/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-800:oklch(27.8% .033 256.848);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--font-weight-medium:500;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.static{position:static}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline-block{display:inline-block}.table{display:table}.h-px{height:1px}.w-full{width:100%}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:calc(var(--spacing)*4)}.gap-y-6{row-gap:calc(var(--spacing)*6)}.rounded-full{border-radius:3.40282e38px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.text-center{text-align:center}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-gray-500{color:var(--color-gray-500)}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark\:text-gray-100{color:var(--color-gray-100)}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}

================================================
FILE: resources/lang/en/auth.php
================================================
<?php

return [
    'login-via' => 'Or log in via',

    'login-failed' => 'Login failed, please try again.',

    'user-not-allowed' => 'Your email is not part of a domain that is allowed.',

    'registration-not-enabled' => 'Registration of a new user is not allowed.',
];


================================================
FILE: resources/views/.gitkeep
================================================


================================================
FILE: resources/views/components/buttons.blade.php
================================================
<div
    x-data="{}"
    x-load-css="[@js(\Filament\Support\Facades\FilamentAsset::getStyleHref('filament-socialite-styles', package: 'filament-socialite'))]"
>
    <div class="flex flex-col gap-y-6">
        @if ($messageBag->isNotEmpty())
            @foreach($messageBag->all() as $value)
                <p class="fi-fo-field-wrp-error-message text-danger-600 dark:text-danger-400">{{ __($value) }}</p>
            @endforeach
        @endif

        @if (count($visibleProviders))
            @if($showDivider)
                <div class="relative flex items-center justify-center text-center">
                    <div class="absolute border-t border-gray-200 w-full h-px"></div>
                    <p class="inline-block relative bg-white text-sm p-2 rounded-full font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-100">
                        {{ __('filament-socialite::auth.login-via') }}
                    </p>
                </div>
            @endif

            <div class="grid @if(count($visibleProviders) > 1) grid-cols-2 @endif gap-4">
                @foreach($visibleProviders as $key => $provider)
                    <x-filament::button
                        :color="$provider->getColor()"
                        :outlined="$provider->getOutlined()"
                        :icon="$provider->getIcon()"
                        tag="a"
                        :href="route($socialiteRoute, $key)"
                        :spa-mode="false"
                    >
                        {{ $provider->getLabel() }}
                    </x-filament::button>
                @endforeach
            </div>
        @else
            <span></span>
        @endif
    </div>
</div>


================================================
FILE: routes/web.php
================================================
<?php

use DutchCodingCompany\FilamentSocialite\Http\Controllers\SocialiteLoginController;
use DutchCodingCompany\FilamentSocialite\Http\Middleware\PanelFromUrlQuery;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Route;

foreach (Filament::getPanels() as $panel) {
    if (! $panel->hasPlugin('filament-socialite')) {
        continue;
    }

    // Retrieve slug for route name.
    $slug = $panel->getPlugin('filament-socialite')->getSlug();

    $domains = $panel->getDomains();

    foreach ((empty($domains) ? [null] : $domains) as $domain) {
        Filament::currentDomain($domain);

        Route::domain($domain)
            ->middleware($panel->getMiddleware())
            ->name("socialite.{$panel->generateRouteName('oauth.redirect')}")
            ->get("/$slug/oauth/{provider}", [SocialiteLoginController::class, 'redirectToProvider']);

        Route::domain($domain)
            ->match(['get', 'post'], "$slug/oauth/callback/{provider}", [SocialiteLoginController::class, 'processCallback'])
            ->middleware([
                ...$panel->getMiddleware(),
                ...config('filament-socialite.middleware'),
            ])
            ->name("socialite.{$panel->generateRouteName('oauth.callback')}");

        Filament::currentDomain(null);
    }
}

/**
 * @note This route can only distinguish between Filament panels using the `state` input. If you have a stateless OAuth
 * implementation, use the "$slug/oauth/callback/{provider}" route instead which has the panel in the URL itself.
 */
Route::match(['get', 'post'], "/oauth/callback/{provider}", [SocialiteLoginController::class, 'processCallback'])
    ->middleware([
        PanelFromUrlQuery::class,
        ...config('filament-socialite.middleware'),
    ])
    ->name('oauth.callback');


================================================
FILE: src/Events/InvalidState.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Two\InvalidStateException;

class InvalidState
{
    use Dispatchable;
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public InvalidStateException $exception,
    ) {
    }
}


================================================
FILE: src/Events/Login.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Events;

use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as SocialiteUser;

class Login
{
    use Dispatchable;
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public FilamentSocialiteUserContract $socialiteUser,
        public SocialiteUser $oauthUser,
    ) {
    }
}


================================================
FILE: src/Events/Registered.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Events;

use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

class Registered
{
    use Dispatchable;
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public string $provider,
        public SocialiteUserContract $oauthUser,
        public FilamentSocialiteUserContract $socialiteUser,
    ) {
    }
}


================================================
FILE: src/Events/RegistrationNotEnabled.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Events;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

class RegistrationNotEnabled
{
    use Dispatchable;
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public string $provider,
        public SocialiteUserContract $oauthUser,
        public ?Authenticatable $user,
    ) {
    }
}


================================================
FILE: src/Events/SocialiteUserConnected.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Events;

use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

class SocialiteUserConnected
{
    use Dispatchable;
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public string $provider,
        public SocialiteUserContract $oauthUser,
        public FilamentSocialiteUserContract $socialiteUser,
    ) {
    }
}


================================================
FILE: src/Events/UserNotAllowed.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

class UserNotAllowed
{
    use Dispatchable;
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(
        public SocialiteUserContract $oauthUser,
    ) {
    }
}


================================================
FILE: src/Exceptions/GuardNotStateful.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Exceptions;

use LogicException;

class GuardNotStateful extends LogicException
{
    public static function make(string $guard): self
    {
        return new self('Guard "'.$guard.'" is not stateful.');
    }
}


================================================
FILE: src/Exceptions/ImplementationException.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Exceptions;

class ImplementationException extends \Exception
{
    //
}


================================================
FILE: src/Exceptions/InvalidCallbackPayload.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Exceptions;

use LogicException;
use Throwable;

class InvalidCallbackPayload extends LogicException
{
    public function __construct(?Throwable $previous = null)
    {
        parent::__construct('The panel could not be decrypted from the OAuth callback.', 0, $previous);
    }

    public static function make(): self
    {
        return new self(...func_get_args());
    }
}


================================================
FILE: src/Exceptions/ProviderNotConfigured.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Exceptions;

use LogicException;

class ProviderNotConfigured extends LogicException
{
    public static function make(string $provider): self
    {
        return new self('Provider "'.$provider.'" is not configured, please configure it in config/services.php and/or on your panel.');
    }
}


================================================
FILE: src/FilamentSocialitePlugin.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite;

use Closure;
use DutchCodingCompany\FilamentSocialite\Exceptions\GuardNotStateful;
use DutchCodingCompany\FilamentSocialite\Exceptions\ImplementationException;
use DutchCodingCompany\FilamentSocialite\Exceptions\ProviderNotConfigured;
use Filament\Contracts\Plugin;
use Filament\Facades\Filament;
use Filament\Panel;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class FilamentSocialitePlugin implements Plugin
{
    use Traits\Callbacks;
    use Traits\Routes;
    use Traits\Models;

    /**
     * @var array<string, \DutchCodingCompany\FilamentSocialite\Provider>
     */
    protected array $providers = [];

    /**
     * @var array<string>
     */
    protected array $domainAllowList = [];

    protected bool $rememberLogin = false;

    /**
     * @phpstan-var (\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, ?\Illuminate\Contracts\Auth\Authenticatable $user): bool) | bool
     */
    protected Closure | bool $registration = false;

    protected ?string $slug = null;

    protected ?string $panelId = null;

    protected bool $showDivider = true;

    public function __construct(
        protected Repository $config,
        protected Factory $auth,
    ) {
        //
    }

    public static function make(): static
    {
        return app(static::class);
    }

    public static function current(): static
    {
        if (Filament::getCurrentPanel()?->hasPlugin('filament-socialite')) {
            /** @var static $plugin */
            $plugin = Filament::getCurrentPanel()->getPlugin('filament-socialite');

            return $plugin;
        }

        throw new ImplementationException('No current panel found with filament-socialite plugin.');
    }

    public function getId(): string
    {
        return 'filament-socialite';
    }

    public function register(Panel $panel): void
    {
        $this->panelId = $panel->getId();
    }

    public function boot(Panel $panel): void
    {
        //
    }

    /**
     * @param array<array-key, \DutchCodingCompany\FilamentSocialite\Provider> $providers
     */
    public function providers(array $providers): static
    {
        // Assign providers as key-value pairs with the provider name as the key.
        $this->providers = Arr::mapWithKeys(
            $providers,
            static fn (Provider $value) => [$value->getName() => $value],
        );

        return $this;
    }

    /**
     * @return array<string, \DutchCodingCompany\FilamentSocialite\Provider>
     */
    public function getProviders(): array
    {
        return $this->providers;
    }

    public function getProvider(string $provider): Provider
    {
        if (! $this->isProviderConfigured($provider)) {
            throw ProviderNotConfigured::make($provider);
        }

        return $this->providers[$provider];
    }

    public function slug(?string $slug): static
    {
        $this->slug = $slug;

        return $this;
    }

    public function getSlug(): string
    {
        return $this->slug ?? rtrim($this->getPanel()->getPath(), '/');
    }

    public function rememberLogin(bool $value): static
    {
        $this->rememberLogin = $value;

        return $this;
    }

    public function getRememberLogin(): bool
    {
        return $this->rememberLogin;
    }

    /**
     * @param (\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, ?\Illuminate\Contracts\Auth\Authenticatable $user): bool) | bool $value
     * @return $this
     */
    public function registration(Closure | bool $value = true): static
    {
        $this->registration = $value;

        return $this;
    }

    /**
     * @return (\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, ?\Illuminate\Contracts\Auth\Authenticatable $user): bool) | bool
     */
    public function getRegistration(): Closure | bool
    {
        return $this->registration;
    }

    /**
     * @param array<string> $values
     */
    public function domainAllowList(array $values): static
    {
        $this->domainAllowList = $values;

        return $this;
    }

    /**
     * @return array<string>
     */
    public function getDomainAllowList(): array
    {
        return $this->domainAllowList;
    }

    public function isProviderConfigured(string $provider): bool
    {
        return $this->config->has('services.'.$provider) && isset($this->providers[$provider]);
    }

    public function showDivider(bool $divider): static
    {
        $this->showDivider = $divider;

        return $this;
    }

    public function getShowDivider(): bool
    {
        return $this->showDivider;
    }

    public function getPanel(): Panel
    {
        return Filament::getPanel($this->getPanelId());
    }

    public function getPanelId(): string
    {
        return $this->panelId ?? throw new ImplementationException('Panel ID not set.');
    }

    public function getGuard(): StatefulGuard
    {
        $guard = $this->auth->guard(
            $guardName = $this->getPanel()->getAuthGuard()
        );

        if ($guard instanceof StatefulGuard) {
            return $guard;
        }

        throw GuardNotStateful::make($guardName);
    }
}


================================================
FILE: src/FilamentSocialiteServiceProvider.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite;

use DutchCodingCompany\FilamentSocialite\View\Components\Buttons;
use Filament\Facades\Filament;
use Filament\Support\Assets\Css;
use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Facades\FilamentView;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Support\Facades\Blade;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;

class FilamentSocialiteServiceProvider extends PackageServiceProvider
{
    public function configurePackage(Package $package): void
    {
        $package
            ->name('filament-socialite')
            ->hasConfigFile()
            ->hasTranslations()
            ->hasViews()
            ->hasRoute('web')
            ->hasMigration('create_socialite_users_table');
    }

    public function packageRegistered(): void
    {
        //
    }

    public function packageBooted(): void
    {
        Blade::componentNamespace('DutchCodingCompany\FilamentSocialite\View\Components', 'filament-socialite');
        Blade::component('buttons', Buttons::class);

        FilamentAsset::register([
            Css::make('filament-socialite-styles', __DIR__.'/../resources/dist/plugin.css')->loadedOnRequest(),
        ], package: 'filament-socialite');

        FilamentView::registerRenderHook(
            'panels::auth.login.form.after',
            static function (): ?string {
                $panel = Filament::getCurrentPanel();

                if (! $panel?->hasPlugin('filament-socialite')) {
                    return null;
                }

                /** @var \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin */
                $plugin = $panel->getPlugin('filament-socialite');

                return Blade::render('<x-filament-socialite::buttons :show-divider="'.($plugin->getShowDivider() ? 'true' : 'false').'" />');
            },
        );

        if (
            version_compare(app()->version(), '11.0', '>=')
            && method_exists(VerifyCsrfToken::class, 'except')
        ) {
            VerifyCsrfToken::except([
                '*/oauth/callback/*',
                'oauth/callback/*',
            ]);
        }
    }
}


================================================
FILE: src/Http/Controllers/SocialiteLoginController.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Http\Controllers;

use DutchCodingCompany\FilamentSocialite\Events;
use DutchCodingCompany\FilamentSocialite\Exceptions\ProviderNotConfigured;
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Http\Middleware\PanelFromUrlQuery;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Filament\Support\Concerns\EvaluatesClosures;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\DB;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\InvalidStateException;
use Symfony\Component\HttpFoundation\Response;

class SocialiteLoginController extends Controller
{
    use EvaluatesClosures;

    private ?FilamentSocialitePlugin $plugin = null;

    public function redirectToProvider(string $provider): mixed
    {
        if (! $this->plugin()->isProviderConfigured($provider)) {
            throw ProviderNotConfigured::make($provider);
        }

        /** @var \Laravel\Socialite\Two\AbstractProvider $driver */
        $driver = Socialite::driver($provider);

        $response = $driver
            ->with([
                ...$this->plugin()->getProvider($provider)->getWith(),
                'state' => $state = PanelFromUrlQuery::encrypt($this->plugin()->getPanel()->getId()),
            ])
            ->scopes($this->plugin()->getProvider($provider)->getScopes())
            ->redirect();

        // Set state value to be equal to the encrypted panel id. This value is used to
        // retrieve the panel id once the authentication returns to our application,
        // and it still prevents CSRF as it is non-guessable value.
        session()->put('state', $state);

        return $response;
    }

    protected function retrieveOauthUser(string $provider): ?SocialiteUserContract
    {
        $stateless = $this->plugin()->getProvider($provider)->getStateless();

        try {
            /** @var \Laravel\Socialite\Two\AbstractProvider $driver */
            $driver = Socialite::driver($provider);

            return $stateless
                ? $driver->stateless()->user()
                : $driver->user();
        } catch (InvalidStateException $e) {
            Events\InvalidState::dispatch($e);
        }

        return null;
    }

    protected function retrieveSocialiteUser(string $provider, SocialiteUserContract $oauthUser): ?FilamentSocialiteUserContract
    {
        return $this->plugin()->getSocialiteUserModel()::findForProvider($provider, $oauthUser);
    }

    protected function redirectToLogin(string $message): RedirectResponse
    {
        // Add error message to the session, this way we can show an error message on the form.
        session()->flash('filament-socialite-login-error', __($message));

        return redirect()->route($this->plugin()->getLoginRouteName());
    }

    protected function authorizeUser(SocialiteUserContract $oauthUser): bool
    {
        return app()->call($this->plugin()->getAuthorizeUserUsing(), ['plugin' => $this->plugin(), 'oauthUser' => $oauthUser]);
    }

    protected function loginUser(string $provider, FilamentSocialiteUserContract $socialiteUser, SocialiteUserContract $oauthUser): Response
    {
        // Log the user in
        $this->plugin()->getGuard()->login($socialiteUser->getUser(), $this->plugin()->getRememberLogin());

        // Dispatch the login event
        Events\Login::dispatch($socialiteUser, $oauthUser);

        return app()->call($this->plugin()->getRedirectAfterLoginUsing(), ['provider' => $provider, 'socialiteUser' => $socialiteUser, 'plugin' => $this->plugin]);
    }

    protected function registerSocialiteUser(string $provider, SocialiteUserContract $oauthUser, Authenticatable $user): Response
    {
        // Create a socialite user
        $socialiteUser = $this->plugin()->getSocialiteUserModel()::createForProvider($provider, $oauthUser, $user);

        // Dispatch the socialite user connected event
        Events\SocialiteUserConnected::dispatch($provider, $oauthUser, $socialiteUser);

        // Login the user
        return $this->loginUser($provider, $socialiteUser, $oauthUser);
    }

    protected function registerOauthUser(string $provider, SocialiteUserContract $oauthUser): Response
    {
        $socialiteUser = DB::transaction(function () use ($provider, $oauthUser) {
            // Create a user
            $user = app()->call($this->plugin()->getCreateUserUsing(), ['provider' => $provider, 'oauthUser' => $oauthUser, 'plugin' => $this->plugin]);

            // Create a socialite user
            return $this->plugin()->getSocialiteUserModel()::createForProvider($provider, $oauthUser, $user);
        });

        // Dispatch the registered event
        Events\Registered::dispatch($provider, $oauthUser, $socialiteUser);

        // Login the user
        return $this->loginUser($provider, $socialiteUser, $oauthUser);
    }

    public function processCallback(string $provider): Response
    {
        if (! $this->plugin()->isProviderConfigured($provider)) {
            throw ProviderNotConfigured::make($provider);
        }

        // Try to retrieve existing user
        $oauthUser = $this->retrieveOauthUser($provider);

        if (is_null($oauthUser)) {
            return $this->redirectToLogin('filament-socialite::auth.login-failed');
        }

        // Verify if the user is authorized.
        if (! $this->authorizeUser($oauthUser)) {
            Events\UserNotAllowed::dispatch($oauthUser);

            return $this->redirectToLogin('filament-socialite::auth.user-not-allowed');
        }

        // Try to find a socialite user
        $socialiteUser = $this->retrieveSocialiteUser($provider, $oauthUser);
        if ($socialiteUser) {
            return $this->loginUser($provider, $socialiteUser, $oauthUser);
        }

        // See if a user already exists, but not for this socialite provider
        $user = app()->call($this->plugin()->getResolveUserUsing(), [
            'provider' => $provider,
            'oauthUser' => $oauthUser,
            'plugin' => $this->plugin,
        ]);

        // See if registration is allowed
        if (! $this->evaluate($this->plugin()->getRegistration(), ['provider' => $provider, 'oauthUser' => $oauthUser, 'user' => $user])) {
            Events\RegistrationNotEnabled::dispatch($provider, $oauthUser, $user);

            return $this->redirectToLogin('filament-socialite::auth.registration-not-enabled');
        }

        // Handle registration
        return $user
            ? $this->registerSocialiteUser($provider, $oauthUser, $user)
            : $this->registerOauthUser($provider, $oauthUser);
    }

    protected function plugin(): FilamentSocialitePlugin
    {
        return $this->plugin ??= FilamentSocialitePlugin::current();
    }
}


================================================
FILE: src/Http/Middleware/PanelFromUrlQuery.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Http\Middleware;

use Closure;
use DutchCodingCompany\FilamentSocialite\Exceptions\InvalidCallbackPayload;
use Filament\Http\Middleware\SetUpPanel;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;

/**
 * @note This callback uses the `state` input to determine the correct panel ID. A simpler
 * implementation is to use the "$slug/oauth/callback/{provider}" route instead, which
 * contains the panel ID in the url itself.
 */
class PanelFromUrlQuery
{
    public function handle(Request $request, Closure $next): mixed
    {
        // Set the current panel based on the encrypted panel name in the url query.
        return (new SetUpPanel())->handle($request, $next, static::decrypt($request));
    }

    public static function encrypt(string $panel): string
    {
        return Crypt::encrypt($panel);
    }

    /**
     * @throws InvalidCallbackPayload
     */
    public static function decrypt(Request $request): string
    {
        try {
            if (! is_string($request->input('state'))) {
                throw new DecryptException('State is not a string.');
            }

            return Crypt::decrypt($request->input('state'));
        } catch (DecryptException $e) {
            throw InvalidCallbackPayload::make($e);
        }
    }
}


================================================
FILE: src/Models/Contracts/FilamentSocialiteUser.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Models\Contracts;

use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

interface FilamentSocialiteUser
{
    public function getUser(): Authenticatable;

    public static function findForProvider(string $provider, SocialiteUserContract $oauthUser): ?self;

    public static function createForProvider(string $provider, SocialiteUserContract $oauthUser, Authenticatable $user): self;
}


================================================
FILE: src/Models/SocialiteUser.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Models;

use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

/**
 * @property int $user_id
 * @property string $provider
 * @property int $provider_id
 */
class SocialiteUser extends Model implements FilamentSocialiteUserContract
{
    protected $fillable = [
        'user_id',
        'provider',
        'provider_id',
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Illuminate\Database\Eloquent\Model, $this>
     */
    public function user(): BelongsTo
    {
        /** @var class-string<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> */
        $user = FilamentSocialitePlugin::current()->getUserModelClass();

        return $this->belongsTo($user);
    }

    public function getUser(): Authenticatable
    {
        assert($this->user instanceof Authenticatable);

        return $this->user;
    }

    public static function findForProvider(string $provider, SocialiteUserContract $oauthUser): ?self
    {
        return self::query()
            ->where('provider', $provider)
            ->where('provider_id', $oauthUser->getId())
            ->first();
    }

    public static function createForProvider(string $provider, SocialiteUserContract $oauthUser, Authenticatable $user): self
    {
        return self::query()
            ->create([
                'user_id' => $user->getKey(),
                'provider' => $provider,
                'provider_id' => $oauthUser->getId(),
            ]);
    }
}


================================================
FILE: src/Provider.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite;

use Closure;
use DutchCodingCompany\FilamentSocialite\Traits\CanBeHidden;
use Filament\Support\Colors\Color;
use Filament\Support\Concerns\EvaluatesClosures;
use Illuminate\Support\Str;

class Provider
{
    use EvaluatesClosures;
    use CanBeHidden;

    protected string $name;

    protected string $label;

    protected string | null $icon = null;

    /**
     * @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
     */
    protected string | array | null $color = Color::Gray;

    protected bool $outlined = true;

    /**
     * @var \Closure(): array<mixed> | array<mixed>
     */
    protected Closure | array $scopes = [];

    /**
     * @var \Closure(): array<mixed> | array<mixed>
     */
    protected Closure | array $with = [];

    protected bool $stateless = false;

    public function __construct(string $name)
    {
        $this->name($name);
    }

    public static function make(string $name): static
    {
        return app(static::class, ['name' => $name]);
    }

    /**
     * @param array<string, mixed> $attributes
     */
    public function fill(array $attributes): static
    {
        foreach ($attributes as $key => $value) {
            $this->{$key}($value);
        }

        return $this;
    }

    public function name(string $name): static
    {
        $this->name = $name;

        return $this;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function label(string $label): static
    {
        $this->label = $label;

        return $this;
    }

    public function getLabel(): string
    {
        return $this->label ?? Str::title($this->getName());
    }

    public function icon(string | null $icon): static
    {
        $this->icon = $icon;

        return $this;
    }

    public function getIcon(): string | null
    {
        return $this->icon;
    }

    /**
     * @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null $color
     */
    public function color(string | array | null $color): static
    {
        $this->color = $color;

        return $this;
    }

    /**
     * @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
     */
    public function getColor(): string | array | null
    {
        return $this->color;
    }

    public function outlined(bool $outlined = true): static
    {
        $this->outlined = $outlined;

        return $this;
    }

    public function getOutlined(): bool
    {
        return $this->outlined;
    }

    /**
     * @param Closure(): array<mixed> | array<mixed> $scopes
     */
    public function scopes(Closure | array $scopes): static
    {
        $this->scopes = $scopes;

        return $this;
    }

    /**
     * @return array<mixed>
     */
    public function getScopes(): array
    {
        return $this->evaluate($this->scopes, ['provider' => $this]);
    }

    /**
     * @param Closure(): array<mixed> | array<mixed> $with
     */
    public function with(Closure | array $with): static
    {
        $this->with = $with;

        return $this;
    }

    /**
     * @return array<mixed>
     */
    public function getWith(): array
    {
        return $this->evaluate($this->with, ['provider' => $this]);
    }

    public function stateless(bool $stateless = true): static
    {
        $this->stateless = $stateless;

        return $this;
    }

    public function getStateless(): bool
    {
        return $this->stateless;
    }
}


================================================
FILE: src/Traits/Callbacks.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Traits;

use Closure;
use DutchCodingCompany\FilamentSocialite\Exceptions\ImplementationException;
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use Filament\Facades\Filament;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;

trait Callbacks
{
    /**
     * @phpstan-var ?\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, self $socialite): \Illuminate\Contracts\Auth\Authenticatable
     */
    protected ?Closure $createUserUsing = null;

    /**
     * @phpstan-var ?\Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Symfony\Component\HttpFoundation\Response
     */
    protected ?Closure $redirectAfterLoginUsing = null;

    /**
     * @phpstan-var ?\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): ?(\Illuminate\Contracts\Auth\Authenticatable)
     */
    protected ?Closure $resolveUserUsing = null;

    /**
     * @phpstan-var ?\Closure(\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin, \Laravel\Socialite\Contracts\User $oauthUser): bool
     */
    protected ?Closure $authorizeUserUsing = null;

    /**
     * @param ?\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Illuminate\Contracts\Auth\Authenticatable $callback
     */
    public function createUserUsing(?Closure $callback = null): static
    {
        $this->createUserUsing = $callback;

        return $this;
    }

    /**
     * @return \Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Illuminate\Contracts\Auth\Authenticatable
     */
    public function getCreateUserUsing(): Closure
    {
        return $this->createUserUsing ?? function (
            string $provider,
            SocialiteUserContract $oauthUser,
            FilamentSocialitePlugin $plugin,
        ) {
            /**
             * @var \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> $query
             */
            $query = (new $this->userModelClass())->query();

            return $query->create([
                'name' => $oauthUser->getName(),
                'email' => $oauthUser->getEmail(),
            ]);
        };
    }

    /**
     * @param \Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Illuminate\Http\RedirectResponse $callback
     */
    public function redirectAfterLoginUsing(Closure $callback): static
    {
        $this->redirectAfterLoginUsing = $callback;

        return $this;
    }

    /**
     * @return \Closure(string $provider, \DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser $socialiteUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): \Symfony\Component\HttpFoundation\Response
     */
    public function getRedirectAfterLoginUsing(): Closure
    {
        return $this->redirectAfterLoginUsing ?? function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) {
            if (($panel = $this->getPanel())->hasTenancy()) {
                $tenant = Filament::getUserDefaultTenant($socialiteUser->getUser());

                if (is_null($tenant) && $tenantRegistrationUrl = $panel->getTenantRegistrationUrl()) {
                    return redirect()->intended($tenantRegistrationUrl);
                }

                return redirect()->intended(
                    $panel->getUrl($tenant)
                );
            }

            return redirect()->intended(
                $this->getPanel()->getUrl()
            );
        };
    }

    /**
     * @param ?\Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): ?(\Illuminate\Contracts\Auth\Authenticatable) $callback
     */
    public function resolveUserUsing(?Closure $callback = null): static
    {
        $this->resolveUserUsing = $callback;

        return $this;
    }

    /**
     * @return \Closure(string $provider, \Laravel\Socialite\Contracts\User $oauthUser, \DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin): ?(\Illuminate\Contracts\Auth\Authenticatable)
     */
    public function getResolveUserUsing(): Closure
    {
        return $this->resolveUserUsing ?? function (string $provider, SocialiteUserContract $oauthUser, FilamentSocialitePlugin $plugin) {
            /** @var \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model&\Illuminate\Contracts\Auth\Authenticatable> $model */
            $model = (new $this->userModelClass());

            return $model->where(
                'email',
                $oauthUser->getEmail()
            )->first();
        };
    }

    /**
     * @param ?\Closure(\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin, \Laravel\Socialite\Contracts\User $oauthUser): bool $callback
     */
    public function authorizeUserUsing(?Closure $callback = null): static
    {
        $this->authorizeUserUsing = $callback;

        return $this;
    }

    /**
     * @return \Closure(\DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin $plugin, \Laravel\Socialite\Contracts\User $oauthUser): bool
     */
    public function getAuthorizeUserUsing(): Closure
    {
        return $this->authorizeUserUsing ?? static::checkDomainAllowList(...);
    }

    public static function checkDomainAllowList(FilamentSocialitePlugin $plugin, SocialiteUserContract $oauthUser): bool
    {
        $domains = $plugin->getDomainAllowList();

        // When no domains are specified, all users are allowed
        if (count($domains) < 1) {
            return true;
        }

        // Get the domain of the email for the specified user
        $emailDomain = Str::of($oauthUser->getEmail() ?? throw new ImplementationException('User email is required.'))
            ->afterLast('@')
            ->lower()
            ->__toString();

        // See if everything after @ is in the domains array
        return in_array($emailDomain, $domains);
    }
}


================================================
FILE: src/Traits/CanBeHidden.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Traits;

use Closure;

trait CanBeHidden
{
    protected bool | Closure $isHidden = false;

    protected bool | Closure $isVisible = true;

    public function hidden(bool | Closure $condition = true): static
    {
        $this->isHidden = $condition;

        return $this;
    }

    public function visible(bool | Closure $condition = true): static
    {
        $this->isVisible = $condition;

        return $this;
    }

    public function isHidden(): bool
    {
        if ($this->evaluate($this->isHidden)) {
            return true;
        }

        return ! $this->evaluate($this->isVisible);
    }

    public function isVisible(): bool
    {
        return ! $this->isHidden();
    }
}


================================================
FILE: src/Traits/Models.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Traits;

use DutchCodingCompany\FilamentSocialite\Exceptions\ImplementationException;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser;

trait Models
{
    /**
     * @var class-string<\Illuminate\Contracts\Auth\Authenticatable>
     */
    protected string $userModelClass = \App\Models\User::class;

    /**
     * @var class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser>
     */
    protected string $socialiteUserModelClass = SocialiteUser::class;

    /**
     * @param class-string<\Illuminate\Contracts\Auth\Authenticatable> $value
     */
    public function userModelClass(string $value): static
    {
        $this->userModelClass = $value;

        return $this;
    }

    /**
     * @return class-string<\Illuminate\Contracts\Auth\Authenticatable>
     */
    public function getUserModelClass(): string
    {
        return $this->userModelClass;
    }

    /**
     * @param class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser> $value
     */
    public function socialiteUserModelClass(string $value): static
    {
        $this->socialiteUserModelClass = $value;

        return $this;
    }

    /**
     * @return class-string<\DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser>
     */
    public function getSocialiteUserModelClass(): string
    {
        return $this->socialiteUserModelClass;
    }

    public function getSocialiteUserModel(): FilamentSocialiteUserContract
    {
        return new ($this->getSocialiteUserModelClass());
    }
}


================================================
FILE: src/Traits/Routes.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Traits;

trait Routes
{
    protected ?string $loginRouteName = null;

    protected ?string $dashboardRouteName = null;

    public function getRoute(): string
    {
        return "socialite.{$this->getPanel()->generateRouteName('oauth.redirect')}";
    }

    public function loginRouteName(string $value): static
    {
        $this->loginRouteName = $value;

        return $this;
    }

    public function getLoginRouteName(): string
    {
        return $this->loginRouteName ?? $this->getPanel()->generateRouteName('auth.login');
    }

    public function dashboardRouteName(string $value): static
    {
        $this->dashboardRouteName = $value;

        return $this;
    }

    public function getDashboardRouteName(): string
    {
        return $this->dashboardRouteName ?? $this->getPanel()->generateRouteName('pages.dashboard');
    }
}


================================================
FILE: src/View/Components/Buttons.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\View\Components;

use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;
use Illuminate\Support\MessageBag;
use Illuminate\View\Component;

class Buttons extends Component
{
    protected FilamentSocialitePlugin $plugin;

    public function __construct(
        public bool $showDivider = true,
    ) {
        $this->plugin = FilamentSocialitePlugin::current();
    }

    /**
     * @inheritDoc
     */
    public function render()
    {
        $messageBag = new MessageBag();

        if (session()->has('filament-socialite-login-error')) {
            $messageBag->add('login-failed', session()->pull('filament-socialite-login-error'));
        }

        return view('filament-socialite::components.buttons', [
            'providers' => $providers = $this->plugin->getProviders(),
            'visibleProviders' => array_filter($providers, fn (Provider $provider) => $provider->isVisible()),
            'socialiteRoute' => $this->plugin->getRoute(),
            'messageBag' => $messageBag,
        ]);
    }
}


================================================
FILE: tailwind.config.js
================================================
module.exports = {
  content: ["./resources/views/**/*.blade.php"],
  corePlugins: {
    preflight: false,
  },
}


================================================
FILE: tests/Fixtures/TestSocialiteUser.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests\Fixtures;

use Laravel\Socialite\Contracts\User;

class TestSocialiteUser implements User
{
    public string $email = 'test@example.com';

    public function getId()
    {
        return 'test-socialite-user-id';
    }

    public function getNickname()
    {
        return 'test-socialite-user-nickname';
    }

    public function getName()
    {
        return 'test-socialite-user-name';
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function getAvatar()
    {
        return null;
    }
}


================================================
FILE: tests/Fixtures/TestTeam.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests\Fixtures;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;

class TestTeam extends Model
{
    protected $table = 'teams';
}


================================================
FILE: tests/Fixtures/TestTenantUser.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests\Fixtures;

use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;

/**
 * @property Collection<\DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam> $teams
 */
class TestTenantUser extends TestUser implements HasTenants
{
    public function canAccessTenant(Model $tenant): bool
    {
        return $this->teams->contains($tenant);
    }

    /**
     * @return \Illuminate\Support\Collection<int, \DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam>
     */
    public function getTenants(Panel $panel): Collection
    {
        return $this->teams;
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam, $this>
     */
    public function teams(): BelongsToMany
    {
        return $this->belongsToMany(TestTeam::class, 'team_user', 'user_id', 'team_id');
    }
}


================================================
FILE: tests/Fixtures/TestUser.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests\Fixtures;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;

class TestUser extends Model implements Authenticatable
{
    protected $table = 'users';

    protected $guarded = [];

    public function getAuthIdentifierName(): string
    {
        return 'test-user-auth-identifier-name';
    }

    public function getAuthIdentifier(): string
    {
        return 'test-user-auth-identifier';
    }

    public function getAuthPassword(): string
    {
        return 'test-user-auth-password';
    }

    public function getAuthPasswordName(): string
    {
        return 'test-user-auth-password';
    }

    public function getRememberToken(): string
    {
        return 'test-user-remember-token';
    }

    public function setRememberToken($value): void
    {
        //
    }

    public function getRememberTokenName(): string
    {
        return 'test-user-remember-token-name';
    }
}


================================================
FILE: tests/Fixtures/change_nullable_password_on_users_table.php
================================================
<?php

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

return new class() extends Migration {
    public function up(): void
    {
        Schema::table('users', static function (Blueprint $table): void {
            $table->string('password')->nullable()->change();
        });
    }

    public function down(): void
    {
        //
    }
};


================================================
FILE: tests/Fixtures/create_socialite_users_table.php
================================================
<?php

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

return new class() extends Migration {
    public function up(): void
    {
        Schema::create('socialite_users', function (Blueprint $table) {
            $table->id();

            $table->foreignId('user_id');
            $table->string('provider');
            $table->string('provider_id');

            $table->timestamps();

            $table->unique([
                'provider',
                'provider_id',
            ]);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('socialite_users');
    }
};


================================================
FILE: tests/Fixtures/create_team_user_table.php
================================================
<?php

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

return new class() extends Migration {
    public function up(): void
    {
        Schema::create('team_user', function (Blueprint $table) {
            $table->integer('team_id');
            $table->integer('user_id');

            $table->unique(['team_id', 'user_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('team_user');
    }
};


================================================
FILE: tests/Fixtures/create_teams_table.php
================================================
<?php

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

return new class() extends Migration {
    public function up(): void
    {
        Schema::create('teams', function (Blueprint $table) {
            $table->id();
            $table->string('name');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('teams');
    }
};


================================================
FILE: tests/SocialiteLoginAuthorizationTest.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests;

use DutchCodingCompany\FilamentSocialite\Events\UserNotAllowed;
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider as PluginProvider;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestSocialiteUser;
use Filament\Facades\Filament;
use Filament\Pages\Dashboard;
use Filament\Panel;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
use Laravel\Socialite\Facades\Socialite;
use LogicException;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;

class SocialiteLoginAuthorizationTest extends TestCase
{
    protected function registerTestPanel(): void
    {
        Filament::registerPanel(
            fn (): Panel => Panel::make()
                ->default()
                ->id($this::getPanelName())
                ->path($this::getPanelName())
                ->tenant(...$this->tenantArguments)
                ->login()
                ->pages([
                    Dashboard::class,
                ])
                ->plugins([
                    FilamentSocialitePlugin::make()
                        ->providers([
                            PluginProvider::make('github')
                                ->label('GitHub')
                                ->icon('fab-github')
                                ->color('danger')
                                ->outlined(false),
                            PluginProvider::make('gitlab')
                                ->label('GitLab')
                                ->icon('fab-gitlab')
                                ->color('danger')
                                ->outlined()
                                ->scopes([])
                                ->with([]),
                        ])
                        ->registration(true)
                        ->userModelClass($this->userModelClass)
                        ->authorizeUserUsing(function (FilamentSocialitePlugin $plugin, SocialiteUserContract $oauthUser) {
                            return $oauthUser->getEmail() === 'test@example.com';
                        }),
                ]),
        );
    }

    #[DataProvider('loginDataProvider')]
    public function testAuthorizationLogin(string $email, bool $dispatchesUserNotAllowedEvent): void
    {
        Event::fake();

        $response = $this
            ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github']))
            ->assertStatus(302);

        $state = session()->get('state');

        $location = $response->headers->get('location') ?? throw new LogicException('Location header not set.');

        parse_str($location, $urlQuery);

        // Test if the correct state is sent to the endpoint in the "Location" header.
        $this->assertEquals($state, $urlQuery['state']);

        // Assert decrypting of the state gives the correct panel name.
        $this->assertEquals($this::getPanelName(), Crypt::decrypt($state));

        $user = new TestSocialiteUser();
        $user->email = $email;

        Socialite::shouldReceive('driver')
            ->with('github')
            ->andReturn(static::makeOAuthProviderMock(
                request()->merge(['state' => $state]),
                $user
            ));

        // Fake oauth response.
        $response = $this
            ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.callback", ['provider' => 'github', 'state' => $state]))
            ->assertStatus(302);

        $dispatchesUserNotAllowedEvent
            ? Event::assertDispatched(UserNotAllowed::class)
            : Event::assertNotDispatched(UserNotAllowed::class);
    }

    /**
     * @return array<string, array{0: string, 1: bool}>
     */
    public static function loginDataProvider(): array
    {
        return [
            'User is authorized to use the application so UserNotAllowedEvent should not be dispatched' => [
                'test@example.com',
                false,
            ],
            'User is not authorized to use the application so UserNotAllowedEvent should be dispatched' => [
                'test@example1.com',
                true,
            ],
        ];
    }
}


================================================
FILE: tests/SocialiteLoginTest.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests;

use Closure;
use DutchCodingCompany\FilamentSocialite\Events\InvalidState;
use DutchCodingCompany\FilamentSocialite\Events\RegistrationNotEnabled;
use DutchCodingCompany\FilamentSocialite\Events\UserNotAllowed;
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestSocialiteUser;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
use Laravel\Socialite\Facades\Socialite;
use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;

class SocialiteLoginTest extends TestCase
{
    #[DataProvider('loginDataProvider')]
    public function testLogin(
        string $email,
        string $callbackRoute,
        ?string $overrideState = null,
        ?string $dispatchedErrorEvent = null,
    ): void {
        Event::fake();

        $response = $this
            ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github']))
            ->assertStatus(302);

        $state = session()->get('state');

        $location = $response->headers->get('location') ?? throw new LogicException('Location header not set.');

        parse_str($location, $urlQuery);

        // Test if the correct state is sent to the endpoint in the "Location" header.
        $this->assertEquals($state, $urlQuery['state']);

        // Assert decrypting of the state gives the correct panel name.
        $this->assertEquals($this::getPanelName(), Crypt::decrypt($state));

        $user = new TestSocialiteUser();
        $user->email = $email;

        Socialite::shouldReceive('driver')
            ->with('github')
            ->andReturn(static::makeOAuthProviderMock(
                request()->merge(['state' => $overrideState ?? $state]),
                $user
            ));

        // Fake oauth response.
        $response = $this
            ->getJson(route($callbackRoute, ['provider' => 'github', 'state' => $state]))
            ->assertStatus(302);

        if ($dispatchedErrorEvent) {
            Event::assertDispatched($dispatchedErrorEvent);

            $this->assertDatabaseMissing('socialite_users', [
                'provider' => 'github',
                'provider_id' => 'test-socialite-user-id',
            ]);

            $this->assertDatabaseMissing('users', [
                'name' => 'test-socialite-user-name',
                'email' => $user->email,
            ]);
        } else {
            $this->assertDatabaseHas('socialite_users', [
                'provider' => 'github',
                'provider_id' => 'test-socialite-user-id',
            ]);

            $this->assertDatabaseHas('users', [
                'name' => 'test-socialite-user-name',
                'email' => $user->email,
            ]);
        }
    }

    /**
     * @return array<string, array{0: string, 1: string, 2: ?string, 3: ?string}>
     */
    public static function loginDataProvider(): array
    {
        return [
            'Login fails when incorrect state (panelized callback route)' => [
                'test@example.com',
                // Use the new callback route that already contains the panel in the url.
                'socialite.filament.'.static::getPanelName().'.oauth.callback',
                'invalid-mocked-state',
                InvalidState::class,
            ],
            'Login fails when incorrect state (general callback route)' => [
                'test@example.com',
                // Use the old callback route that determines the panel based on the state parameter.
                'oauth.callback',
                'invalid-mocked-state',
                InvalidState::class,
            ],
            'Login succeeds when email in domain allow list' => [
                'test@example.com',
                'socialite.filament.'.static::getPanelName().'.oauth.callback',
                null,
                null,
            ],
            'Login fails when email not in domain allow list' => [
                'test@example1.com',
                'socialite.filament.'.static::getPanelName().'.oauth.callback',
                null,
                UserNotAllowed::class,
            ],
        ];
    }

    #[DataProvider('registrationBlockProvider')]
    public function testRegistrationBlock(bool $createUser, Closure | bool $registrationEnabled): void
    {
        Event::fake();

        if ($createUser) {
            DB::table('users')->insert([
                'name' => 'test-user',
                'email' => 'test@example.com',
            ]);
        }

        FilamentSocialitePlugin::current()->registration($registrationEnabled);

        $this
            ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github']))
            ->assertStatus(302);

        $state = session()->get('state');

        Socialite::shouldReceive('driver')
            ->with('github')
            ->andReturn(static::makeOAuthProviderMock(
                request()->merge(['state' => $state]),
                new TestSocialiteUser()
            ));

        // Fake oauth response.
        $this
            ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.callback", ['provider' => 'github', 'state' => $state]))
            ->assertStatus(302);

        if (! $createUser) {
            // If there is no user, the event should have been dispatched since the plugin option disabled registration.
            Event::assertDispatched(RegistrationNotEnabled::class);
        }
    }

    /**
     * @return array<string, array<mixed>>
     */
    public static function registrationBlockProvider(): array
    {
        $callback = function (string $provider, SocialiteUserContract $oauthUser, ?Authenticatable $user) {
            return (bool) $user;
        };

        return [
            'Authenticatable exists for socialite user' => [true, $callback],
            'Authenticatable does not exist for socialite user' => [false, $callback],
            'Registration is always blocked' => [true, false],
        ];
    }
}


================================================
FILE: tests/SocialiteStatelessLoginTest.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests;

use DutchCodingCompany\FilamentSocialite\Events\InvalidState;
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Provider;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestSocialiteUser;
use Filament\Facades\Filament;
use Filament\Pages\Dashboard;
use Filament\Panel;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Facades\Socialite;
use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;

class SocialiteStatelessLoginTest extends TestCase
{
    protected function registerTestPanel(): void
    {
        Filament::registerPanel(
            fn (): Panel => Panel::make()
                ->default()
                ->id($this::getPanelName())
                ->path($this::getPanelName())
                ->tenant(...$this->tenantArguments)
                ->login()
                ->pages([
                    Dashboard::class,
                ])
                ->plugins([
                    FilamentSocialitePlugin::make()
                        ->providers([
                            Provider::make('github')
                                ->label('GitHub')
                                ->icon('fab-github')
                                ->color('danger')
                                ->outlined(false)
                                ->stateless(),
                            Provider::make('gitlab')
                                ->label('GitLab')
                                ->icon('fab-gitlab')
                                ->color('danger')
                                ->outlined()
                                ->scopes([])
                                ->with([]),
                        ])
                        ->registration(true)
                        ->userModelClass($this->userModelClass)
                        ->domainAllowList(['example.com']),
                ]),
        );
    }

    #[DataProvider('statelessLoginDataProvider')]
    public function testStatelessLogin(
        string $email,
        string $callbackRoute,
        ?string $overrideState = null,
        ?string $event = null,
    ): void {
        Event::fake();

        $response = $this
            ->getJson(route("socialite.filament.{$this::getPanelName()}.oauth.redirect", ['provider' => 'github']))
            ->assertStatus(302);

        $state = session()->get('state');

        $location = $response->headers->get('location') ?? throw new LogicException('Location header not set.');

        parse_str($location, $urlQuery);

        // Test if the correct state is sent to the endpoint in the "Location" header.
        $this->assertEquals($state, $urlQuery['state']);

        // Assert decrypting of the state gives the correct panel name.
        $this->assertEquals($this::getPanelName(), Crypt::decrypt($state));

        $user = new TestSocialiteUser();
        $user->email = $email;

        Socialite::shouldReceive('driver')
            ->with('github')
            ->andReturn(static::makeOAuthProviderMock(
                request()->merge(['state' => $overrideState ?? $state]),
                $user
            ));

        // Fake oauth response.
        $response = $this
            ->getJson(route($callbackRoute, ['provider' => 'github', 'state' => $state]))
            ->assertStatus(302);

        if ($event !== null) {
            Event::assertNotDispatched($event);
        }

        $this->assertDatabaseHas('socialite_users', [
            'provider' => 'github',
            'provider_id' => 'test-socialite-user-id',
        ]);

        $this->assertDatabaseHas('users', [
            'name' => 'test-socialite-user-name',
            'email' => $user->email,
        ]);
    }

    /**
     * @return array<string, array{0: string, 1: string, 2: ?string, 3: ?string}>
     */
    public static function statelessLoginDataProvider(): array
    {
        return [
            'Stateless login succeeds (panelized callback route)' => [
                'test@example.com',
                // Use the new callback route that already contains the panel in the url.
                'socialite.filament.'.static::getPanelName().'.oauth.callback',
                null,
                InvalidState::class,
            ],
            'Stateless login succeeds with mocked state (general callback route)' => [
                'test@example.com',
                // Use the old callback route that determines the panel based on the state parameter.
                'oauth.callback',
                'invalid-mocked-state',
                InvalidState::class,
            ],
        ];
    }
}


================================================
FILE: tests/SocialiteTenantLoginTest.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests;

use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestSocialiteUser;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTeam;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestTenantUser;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Facades\Socialite;
use LogicException;
use Mockery;

class SocialiteTenantLoginTest extends TestCase
{
    protected string $userModelClass = TestTenantUser::class;

    protected array $tenantArguments = [
        TestTeam::class,
    ];

    public function testTenantLogin(): void
    {
        FilamentSocialitePlugin::current()->redirectAfterLoginUsing(function (string $provider, FilamentSocialiteUserContract $socialiteUser, FilamentSocialitePlugin $plugin) {
            assert($socialiteUser instanceof SocialiteUser);

            $this->assertEquals($this::getPanelName(), $plugin->getPanel()->getId());
            $this->assertEquals('github', $provider);
            $this->assertEquals('github', $socialiteUser->provider);
            $this->assertEquals('test-socialite-user-id', $socialiteUser->provider_id);

            return redirect()->to('/some-tenant-url');
        });

        $response = $this
            ->getJson("/{$this::getPanelName()}/oauth/github")
            ->assertStatus(302);

        $state = session()->get('state');

        Socialite::shouldReceive('driver')
            ->with('github')
            ->andReturn(static::makeOAuthProviderMock(
                request()->merge(['state' => $state]),
                new TestSocialiteUser()
            ));

        // Fake oauth response.
        $response = $this
            ->getJson("/oauth/callback/github?state=$state")
            ->assertStatus(302);

        $this->assertStringContainsString('/some-tenant-url', $response->headers->get('Location') ?? throw new LogicException('Location header not set.'));

        $this->assertDatabaseHas('socialite_users', [
            'provider' => 'github',
            'provider_id' => 'test-socialite-user-id',
        ]);

        $this->assertDatabaseHas('users', [
            'name' => 'test-socialite-user-name',
            'email' => 'test@example.com',
        ]);
    }
}


================================================
FILE: tests/TestCase.php
================================================
<?php

namespace DutchCodingCompany\FilamentSocialite\Tests;

use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
use DutchCodingCompany\FilamentSocialite\FilamentSocialiteServiceProvider;
use DutchCodingCompany\FilamentSocialite\Models\Contracts\FilamentSocialiteUser as FilamentSocialiteUserContract;
use DutchCodingCompany\FilamentSocialite\Models\SocialiteUser;
use DutchCodingCompany\FilamentSocialite\Provider;
use DutchCodingCompany\FilamentSocialite\Tests\Fixtures\TestUser;
use Filament\Facades\Filament;
use Filament\FilamentServiceProvider;
use Filament\Pages\Dashboard;
use Filament\Panel;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Session\Middleware\StartSession;
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
use Laravel\Socialite\SocialiteServiceProvider;
use Laravel\Socialite\Two\AbstractProvider;
use Livewire\LivewireServiceProvider;
use Mockery\LegacyMockInterface;
use Orchestra\Testbench\TestCase as Orchestra;

class TestCase extends Orchestra
{
    /**
     * @var class-string<\Illuminate\Contracts\Auth\Authenticatable>
     */
    protected string $userModelClass = TestUser::class;

    /**
     * @var array{0: ?class-string<\Illuminate\Database\Eloquent\Model>, 1?: ?string, 2?: ?string}
     */
    protected array $tenantArguments = [null];

    protected function setUp(): void
    {
        parent::setUp();

        Filament::setCurrentPanel(self::getPanelName());

        Factory::guessFactoryNamesUsing(
            fn (
                string $modelName,
            ) => 'DutchCodingCompany\\FilamentSocialite\\Database\\Factories\\'.class_basename($modelName).'Factory'
        );

        $this->app?->make(Kernel::class)->pushMiddleware(StartSession::class);
    }

    protected function getPackageProviders($app)
    {
        $this->registerTestPanel();

        return [
            FilamentServiceProvider::class,
            FilamentSocialiteServiceProvider::class,
            SocialiteServiceProvider::class,
            LivewireServiceProvider::class,
        ];
    }

    protected function registerTestPanel(): void
    {
        Filament::registerPanel(
            fn (): Panel => Panel::make()
                ->default()
                ->id($this::getPanelName())
                ->path($this::getPanelName())
                ->tenant(...$this->tenantArguments)
                ->login()
                ->pages([
                    Dashboard::class,
                ])
                ->plugins([
                    FilamentSocialitePlugin::make()
                        ->providers([
                            Provider::make('github')
                                ->label('GitHub')
                                ->icon('fab-github')
                                ->color('danger')
                                ->outlined(false),
                            Provider::make('gitlab')
                                ->label('GitLab')
                                ->icon('fab-gitlab')
                                ->color('danger')
                                ->outlined()
                                ->scopes([])
                                ->with([]),
                        ])
                        ->registration(true)
                        ->userModelClass($this->userModelClass)
                        ->domainAllowList(['example.com']),
                ]),
        );
    }

    public function getEnvironmentSetUp($app)
    {
        config()->set('app.key', 'base64:'.base64_encode(
            Encrypter::generateKey('AES-256-CBC')
        ));

        config()->set('services.github', [
            'client_id' => 'abcdmockedabcd',
            'client_secret' => 'defgmockeddefg',
            'redirect' => 'http://localhost/oauth/callback/github',
        ]);
    }

    protected function defineDatabaseMigrations()
    {
        $this->loadLaravelMigrations();
        $this->loadMigrationsFrom(__DIR__.'/Fixtures');
    }

    protected static function getPanelName(): string
    {
        return 'testpanel';
    }

    protected static function makeOAuthProviderMock(
        Request $request,
        SocialiteUserContract $user,
    ): LegacyMockInterface {
        $mock = \Mockery::mock(
            AbstractProvider::class,
            [$request, 'test-client-id', 'test-client-secret', 'test-redirect-url']
        )
            ->makePartial()
            ->shouldAllowMockingProtectedMethods();

        $mock->shouldReceive('getAccessTokenResponse')->andReturn([]);
        $mock->shouldReceive('getUserByToken')->andReturn([]);
        $mock->shouldReceive('userInstance')->andReturn($user);

        return $mock;
    }
}
Download .txt
gitextract_wtq0hz3c/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── dependabot-auto-merge.yml
│       ├── php-cs-fixer.yml
│       ├── phpstan.yml
│       ├── run-tests.yml
│       └── update-changelog.yml
├── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── UPGRADE.md
├── bin/
│   └── upgrade-v2
├── composer.json
├── config/
│   └── filament-socialite.php
├── database/
│   └── migrations/
│       └── create_socialite_users_table.php.stub
├── package.json
├── phpstan-baseline.neon
├── phpstan.neon.dist
├── phpunit.xml
├── rector.php
├── resources/
│   ├── css/
│   │   └── plugin.css
│   ├── dist/
│   │   └── plugin.css
│   ├── lang/
│   │   └── en/
│   │       └── auth.php
│   └── views/
│       ├── .gitkeep
│       └── components/
│           └── buttons.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Events/
│   │   ├── InvalidState.php
│   │   ├── Login.php
│   │   ├── Registered.php
│   │   ├── RegistrationNotEnabled.php
│   │   ├── SocialiteUserConnected.php
│   │   └── UserNotAllowed.php
│   ├── Exceptions/
│   │   ├── GuardNotStateful.php
│   │   ├── ImplementationException.php
│   │   ├── InvalidCallbackPayload.php
│   │   └── ProviderNotConfigured.php
│   ├── FilamentSocialitePlugin.php
│   ├── FilamentSocialiteServiceProvider.php
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── SocialiteLoginController.php
│   │   └── Middleware/
│   │       └── PanelFromUrlQuery.php
│   ├── Models/
│   │   ├── Contracts/
│   │   │   └── FilamentSocialiteUser.php
│   │   └── SocialiteUser.php
│   ├── Provider.php
│   ├── Traits/
│   │   ├── Callbacks.php
│   │   ├── CanBeHidden.php
│   │   ├── Models.php
│   │   └── Routes.php
│   └── View/
│       └── Components/
│           └── Buttons.php
├── tailwind.config.js
└── tests/
    ├── Fixtures/
    │   ├── TestSocialiteUser.php
    │   ├── TestTeam.php
    │   ├── TestTenantUser.php
    │   ├── TestUser.php
    │   ├── change_nullable_password_on_users_table.php
    │   ├── create_socialite_users_table.php
    │   ├── create_team_user_table.php
    │   └── create_teams_table.php
    ├── SocialiteLoginAuthorizationTest.php
    ├── SocialiteLoginTest.php
    ├── SocialiteStatelessLoginTest.php
    ├── SocialiteTenantLoginTest.php
    └── TestCase.php
Download .txt
SYMBOL INDEX (172 symbols across 35 files)

FILE: src/Events/InvalidState.php
  class InvalidState (line 9) | class InvalidState
    method __construct (line 19) | public function __construct(

FILE: src/Events/Login.php
  class Login (line 10) | class Login
    method __construct (line 20) | public function __construct(

FILE: src/Events/Registered.php
  class Registered (line 10) | class Registered
    method __construct (line 20) | public function __construct(

FILE: src/Events/RegistrationNotEnabled.php
  class RegistrationNotEnabled (line 10) | class RegistrationNotEnabled
    method __construct (line 20) | public function __construct(

FILE: src/Events/SocialiteUserConnected.php
  class SocialiteUserConnected (line 10) | class SocialiteUserConnected
    method __construct (line 20) | public function __construct(

FILE: src/Events/UserNotAllowed.php
  class UserNotAllowed (line 9) | class UserNotAllowed
    method __construct (line 19) | public function __construct(

FILE: src/Exceptions/GuardNotStateful.php
  class GuardNotStateful (line 7) | class GuardNotStateful extends LogicException
    method make (line 9) | public static function make(string $guard): self

FILE: src/Exceptions/ImplementationException.php
  class ImplementationException (line 5) | class ImplementationException extends \Exception

FILE: src/Exceptions/InvalidCallbackPayload.php
  class InvalidCallbackPayload (line 8) | class InvalidCallbackPayload extends LogicException
    method __construct (line 10) | public function __construct(?Throwable $previous = null)
    method make (line 15) | public static function make(): self

FILE: src/Exceptions/ProviderNotConfigured.php
  class ProviderNotConfigured (line 7) | class ProviderNotConfigured extends LogicException
    method make (line 9) | public static function make(string $provider): self

FILE: src/FilamentSocialitePlugin.php
  class FilamentSocialitePlugin (line 18) | class FilamentSocialitePlugin implements Plugin
    method __construct (line 47) | public function __construct(
    method make (line 54) | public static function make(): static
    method current (line 59) | public static function current(): static
    method getId (line 71) | public function getId(): string
    method register (line 76) | public function register(Panel $panel): void
    method boot (line 81) | public function boot(Panel $panel): void
    method providers (line 89) | public function providers(array $providers): static
    method getProviders (line 103) | public function getProviders(): array
    method getProvider (line 108) | public function getProvider(string $provider): Provider
    method slug (line 117) | public function slug(?string $slug): static
    method getSlug (line 124) | public function getSlug(): string
    method rememberLogin (line 129) | public function rememberLogin(bool $value): static
    method getRememberLogin (line 136) | public function getRememberLogin(): bool
    method registration (line 145) | public function registration(Closure | bool $value = true): static
    method getRegistration (line 155) | public function getRegistration(): Closure | bool
    method domainAllowList (line 163) | public function domainAllowList(array $values): static
    method getDomainAllowList (line 173) | public function getDomainAllowList(): array
    method isProviderConfigured (line 178) | public function isProviderConfigured(string $provider): bool
    method showDivider (line 183) | public function showDivider(bool $divider): static
    method getShowDivider (line 190) | public function getShowDivider(): bool
    method getPanel (line 195) | public function getPanel(): Panel
    method getPanelId (line 200) | public function getPanelId(): string
    method getGuard (line 205) | public function getGuard(): StatefulGuard

FILE: src/FilamentSocialiteServiceProvider.php
  class FilamentSocialiteServiceProvider (line 15) | class FilamentSocialiteServiceProvider extends PackageServiceProvider
    method configurePackage (line 17) | public function configurePackage(Package $package): void
    method packageRegistered (line 28) | public function packageRegistered(): void
    method packageBooted (line 33) | public function packageBooted(): void

FILE: src/Http/Controllers/SocialiteLoginController.php
  class SocialiteLoginController (line 20) | class SocialiteLoginController extends Controller
    method redirectToProvider (line 26) | public function redirectToProvider(string $provider): mixed
    method retrieveOauthUser (line 51) | protected function retrieveOauthUser(string $provider): ?SocialiteUser...
    method retrieveSocialiteUser (line 69) | protected function retrieveSocialiteUser(string $provider, SocialiteUs...
    method redirectToLogin (line 74) | protected function redirectToLogin(string $message): RedirectResponse
    method authorizeUser (line 82) | protected function authorizeUser(SocialiteUserContract $oauthUser): bool
    method loginUser (line 87) | protected function loginUser(string $provider, FilamentSocialiteUserCo...
    method registerSocialiteUser (line 98) | protected function registerSocialiteUser(string $provider, SocialiteUs...
    method registerOauthUser (line 110) | protected function registerOauthUser(string $provider, SocialiteUserCo...
    method processCallback (line 127) | public function processCallback(string $provider): Response
    method plugin (line 173) | protected function plugin(): FilamentSocialitePlugin

FILE: src/Http/Middleware/PanelFromUrlQuery.php
  class PanelFromUrlQuery (line 17) | class PanelFromUrlQuery
    method handle (line 19) | public function handle(Request $request, Closure $next): mixed
    method encrypt (line 25) | public static function encrypt(string $panel): string
    method decrypt (line 33) | public static function decrypt(Request $request): string

FILE: src/Models/Contracts/FilamentSocialiteUser.php
  type FilamentSocialiteUser (line 8) | interface FilamentSocialiteUser
    method getUser (line 10) | public function getUser(): Authenticatable;
    method findForProvider (line 12) | public static function findForProvider(string $provider, SocialiteUser...
    method createForProvider (line 14) | public static function createForProvider(string $provider, SocialiteUs...

FILE: src/Models/SocialiteUser.php
  class SocialiteUser (line 17) | class SocialiteUser extends Model implements FilamentSocialiteUserContract
    method user (line 28) | public function user(): BelongsTo
    method getUser (line 36) | public function getUser(): Authenticatable
    method findForProvider (line 43) | public static function findForProvider(string $provider, SocialiteUser...
    method createForProvider (line 51) | public static function createForProvider(string $provider, SocialiteUs...

FILE: src/Provider.php
  class Provider (line 11) | class Provider
    method __construct (line 41) | public function __construct(string $name)
    method make (line 46) | public static function make(string $name): static
    method fill (line 54) | public function fill(array $attributes): static
    method name (line 63) | public function name(string $name): static
    method getName (line 70) | public function getName(): string
    method label (line 75) | public function label(string $label): static
    method getLabel (line 82) | public function getLabel(): string
    method icon (line 87) | public function icon(string | null $icon): static
    method getIcon (line 94) | public function getIcon(): string | null
    method color (line 102) | public function color(string | array | null $color): static
    method getColor (line 112) | public function getColor(): string | array | null
    method outlined (line 117) | public function outlined(bool $outlined = true): static
    method getOutlined (line 124) | public function getOutlined(): bool
    method scopes (line 132) | public function scopes(Closure | array $scopes): static
    method getScopes (line 142) | public function getScopes(): array
    method with (line 150) | public function with(Closure | array $with): static
    method getWith (line 160) | public function getWith(): array
    method stateless (line 165) | public function stateless(bool $stateless = true): static
    method getStateless (line 172) | public function getStateless(): bool

FILE: src/Traits/Callbacks.php
  type Callbacks (line 13) | trait Callbacks
    method createUserUsing (line 38) | public function createUserUsing(?Closure $callback = null): static
    method getCreateUserUsing (line 48) | public function getCreateUserUsing(): Closure
    method redirectAfterLoginUsing (line 70) | public function redirectAfterLoginUsing(Closure $callback): static
    method getRedirectAfterLoginUsing (line 80) | public function getRedirectAfterLoginUsing(): Closure
    method resolveUserUsing (line 104) | public function resolveUserUsing(?Closure $callback = null): static
    method getResolveUserUsing (line 114) | public function getResolveUserUsing(): Closure
    method authorizeUserUsing (line 130) | public function authorizeUserUsing(?Closure $callback = null): static
    method getAuthorizeUserUsing (line 140) | public function getAuthorizeUserUsing(): Closure
    method checkDomainAllowList (line 145) | public static function checkDomainAllowList(FilamentSocialitePlugin $p...

FILE: src/Traits/CanBeHidden.php
  type CanBeHidden (line 7) | trait CanBeHidden
    method hidden (line 13) | public function hidden(bool | Closure $condition = true): static
    method visible (line 20) | public function visible(bool | Closure $condition = true): static
    method isHidden (line 27) | public function isHidden(): bool
    method isVisible (line 36) | public function isVisible(): bool

FILE: src/Traits/Models.php
  type Models (line 9) | trait Models
    method userModelClass (line 24) | public function userModelClass(string $value): static
    method getUserModelClass (line 34) | public function getUserModelClass(): string
    method socialiteUserModelClass (line 42) | public function socialiteUserModelClass(string $value): static
    method getSocialiteUserModelClass (line 52) | public function getSocialiteUserModelClass(): string
    method getSocialiteUserModel (line 57) | public function getSocialiteUserModel(): FilamentSocialiteUserContract

FILE: src/Traits/Routes.php
  type Routes (line 5) | trait Routes
    method getRoute (line 11) | public function getRoute(): string
    method loginRouteName (line 16) | public function loginRouteName(string $value): static
    method getLoginRouteName (line 23) | public function getLoginRouteName(): string
    method dashboardRouteName (line 28) | public function dashboardRouteName(string $value): static
    method getDashboardRouteName (line 35) | public function getDashboardRouteName(): string

FILE: src/View/Components/Buttons.php
  class Buttons (line 10) | class Buttons extends Component
    method __construct (line 14) | public function __construct(
    method render (line 23) | public function render()

FILE: tests/Fixtures/TestSocialiteUser.php
  class TestSocialiteUser (line 7) | class TestSocialiteUser implements User
    method getId (line 11) | public function getId()
    method getNickname (line 16) | public function getNickname()
    method getName (line 21) | public function getName()
    method getEmail (line 26) | public function getEmail()
    method getAvatar (line 31) | public function getAvatar()

FILE: tests/Fixtures/TestTeam.php
  class TestTeam (line 8) | class TestTeam extends Model

FILE: tests/Fixtures/TestTenantUser.php
  class TestTenantUser (line 14) | class TestTenantUser extends TestUser implements HasTenants
    method canAccessTenant (line 16) | public function canAccessTenant(Model $tenant): bool
    method getTenants (line 24) | public function getTenants(Panel $panel): Collection
    method teams (line 32) | public function teams(): BelongsToMany

FILE: tests/Fixtures/TestUser.php
  class TestUser (line 8) | class TestUser extends Model implements Authenticatable
    method getAuthIdentifierName (line 14) | public function getAuthIdentifierName(): string
    method getAuthIdentifier (line 19) | public function getAuthIdentifier(): string
    method getAuthPassword (line 24) | public function getAuthPassword(): string
    method getAuthPasswordName (line 29) | public function getAuthPasswordName(): string
    method getRememberToken (line 34) | public function getRememberToken(): string
    method setRememberToken (line 39) | public function setRememberToken($value): void
    method getRememberTokenName (line 44) | public function getRememberTokenName(): string

FILE: tests/Fixtures/change_nullable_password_on_users_table.php
  method up (line 8) | public function up(): void
  method down (line 15) | public function down(): void

FILE: tests/Fixtures/create_socialite_users_table.php
  method up (line 8) | public function up(): void
  method down (line 26) | public function down(): void

FILE: tests/Fixtures/create_team_user_table.php
  method up (line 8) | public function up(): void
  method down (line 18) | public function down(): void

FILE: tests/Fixtures/create_teams_table.php
  method up (line 8) | public function up(): void
  method down (line 16) | public function down(): void

FILE: tests/SocialiteLoginAuthorizationTest.php
  class SocialiteLoginAuthorizationTest (line 21) | class SocialiteLoginAuthorizationTest extends TestCase
    method registerTestPanel (line 23) | protected function registerTestPanel(): void
    method testAuthorizationLogin (line 60) | #[DataProvider('loginDataProvider')]
    method loginDataProvider (line 104) | public static function loginDataProvider(): array

FILE: tests/SocialiteLoginTest.php
  class SocialiteLoginTest (line 20) | class SocialiteLoginTest extends TestCase
    method testLogin (line 22) | #[DataProvider('loginDataProvider')]
    method loginDataProvider (line 90) | public static function loginDataProvider(): array
    method testRegistrationBlock (line 122) | #[DataProvider('registrationBlockProvider')]
    method registrationBlockProvider (line 163) | public static function registrationBlockProvider(): array

FILE: tests/SocialiteStatelessLoginTest.php
  class SocialiteStatelessLoginTest (line 18) | class SocialiteStatelessLoginTest extends TestCase
    method registerTestPanel (line 20) | protected function registerTestPanel(): void
    method testStatelessLogin (line 56) | #[DataProvider('statelessLoginDataProvider')]
    method statelessLoginDataProvider (line 114) | public static function statelessLoginDataProvider(): array

FILE: tests/SocialiteTenantLoginTest.php
  class SocialiteTenantLoginTest (line 16) | class SocialiteTenantLoginTest extends TestCase
    method testTenantLogin (line 24) | public function testTenantLogin(): void

FILE: tests/TestCase.php
  class TestCase (line 27) | class TestCase extends Orchestra
    method setUp (line 39) | protected function setUp(): void
    method getPackageProviders (line 54) | protected function getPackageProviders($app)
    method registerTestPanel (line 66) | protected function registerTestPanel(): void
    method getEnvironmentSetUp (line 101) | public function getEnvironmentSetUp($app)
    method defineDatabaseMigrations (line 114) | protected function defineDatabaseMigrations()
    method getPanelName (line 120) | protected static function getPanelName(): string
    method makeOAuthProviderMock (line 125) | protected static function makeOAuthProviderMock(
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (148K chars).
[
  {
    "path": ".editorconfig",
    "chars": 220,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_size = 4\nindent_style = space\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".gitattributes",
    "chars": 671,
    "preview": "# Path-based git attributes\n# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n\n# Ignore all test and"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 2338,
    "preview": "name: Bug report\ndescription: Report a problem you're experiencing\nbody:\n  - type: markdown\n    attributes:\n      value:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 495,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question\n    url: https://discord.com/channels/88308379211230"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 322,
    "preview": "# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repositor"
  },
  {
    "path": ".github/workflows/dependabot-auto-merge.yml",
    "chars": 1030,
    "preview": "name: dependabot-auto-merge\non: pull_request_target\n\npermissions:\n  pull-requests: write\n  contents: write\n\njobs:\n  depe"
  },
  {
    "path": ".github/workflows/php-cs-fixer.yml",
    "chars": 507,
    "preview": "name: Check & fix styling\n\non: [push]\n\njobs:\n  php-cs-fixer:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checko"
  },
  {
    "path": ".github/workflows/phpstan.yml",
    "chars": 1489,
    "preview": "name: PHPStan\n\non:\n  push:\n    paths:\n      - '**.php'\n      - .github/workflows/phpstan.yml\n      - phpstan.neon.dist\n "
  },
  {
    "path": ".github/workflows/run-tests.yml",
    "chars": 1732,
    "preview": "name: run-tests\n\non:\n  push:\n    paths:\n      - '**.php'\n      - .github/workflows/run-tests.yml\n      - phpunit.xml.dis"
  },
  {
    "path": ".github/workflows/update-changelog.yml",
    "chars": 645,
    "preview": "name: \"Update Changelog\"\n\non:\n  release:\n    types: [released]\n\njobs:\n  update:\n    runs-on: ubuntu-latest\n\n    steps:\n "
  },
  {
    "path": ".gitignore",
    "chars": 170,
    "preview": ".idea\n.php_cs\n.php_cs.cache\n.phpunit.result.cache\nbuild\ncomposer.lock\npackage-lock.json\ncoverage\ndocs\nphpstan.neon\ntestb"
  },
  {
    "path": ".php-cs-fixer.php",
    "chars": 6085,
    "preview": "<?php\n\nuse PhpCsFixer\\Config;\nuse PhpCsFixer\\Finder;\n\n$rules = [\n    'array_indentation' => true,\n    'array_syntax' => "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 12944,
    "preview": "# Changelog\n\nAll notable changes to `filament-socialite` will be documented in this file.\n\n## [3.2.1 - 2026-04-14](https"
  },
  {
    "path": "LICENSE.md",
    "chars": 1094,
    "preview": "The MIT License (MIT)\n\nCopyright (c) DutchCodingCompany <m@rcoboe.rs>\n\nPermission is hereby granted, free of charge, to "
  },
  {
    "path": "README.md",
    "chars": 15029,
    "preview": "<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://banners.beyondco.de/Filament%20Socialite.png?th"
  },
  {
    "path": "SECURITY.md",
    "chars": 621,
    "preview": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurre"
  },
  {
    "path": "UPGRADE.md",
    "chars": 5340,
    "preview": "# Upgrade Guide\n## `2.x.x` to `3.x.x` (Filament v4.x)\n\n### Slug\nIn v3 of the plugin, the panel's configured path is used"
  },
  {
    "path": "bin/upgrade-v2",
    "chars": 161,
    "preview": "#!/usr/bin/env php\n<?php\n\nnamespace Composer;\n\nexec(\"vendor/bin/rector process --config vendor/dutchcodingcompany/filame"
  },
  {
    "path": "composer.json",
    "chars": 2503,
    "preview": "{\n    \"name\": \"dutchcodingcompany/filament-socialite\",\n    \"description\": \"Social login for Filament through Laravel Soc"
  },
  {
    "path": "config/filament-socialite.php",
    "chars": 680,
    "preview": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | OAuth call"
  },
  {
    "path": "database/migrations/create_socialite_users_table.php.stub",
    "chars": 742,
    "preview": "<?php\n\nuse Illuminate\\Support\\Facades\\Schema;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migratio"
  },
  {
    "path": "package.json",
    "chars": 556,
    "preview": "{\n    \"private\": true,\n    \"name\": \"filament-socialite\",\n    \"devDependencies\": {\n        \"@tailwindcss/cli\": \"^4.1.10\","
  },
  {
    "path": "phpstan-baseline.neon",
    "chars": 2947,
    "preview": "parameters:\n\tignoreErrors:\n\t\t-\n\t\t\tmessage: '#^Class App\\\\Models\\\\User not found\\.$#'\n\t\t\tidentifier: class.notFound\n\t\t\tco"
  },
  {
    "path": "phpstan.neon.dist",
    "chars": 192,
    "preview": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./phpstan-baseline.neon\n\nparameters:\n    level: 8\n\n    p"
  },
  {
    "path": "phpunit.xml",
    "chars": 955,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNam"
  },
  {
    "path": "rector.php",
    "chars": 2681,
    "preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Rector\\Config\\RectorConfig;\nuse Utils\\Rector\\Rector\\RenameMethods;\n\nreturn static f"
  },
  {
    "path": "resources/css/plugin.css",
    "chars": 60,
    "preview": "@import \"tailwindcss\";\n\n@config '../../tailwind.config.js';\n"
  },
  {
    "path": "resources/dist/plugin.css",
    "chars": 5711,
    "preview": "/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none))"
  },
  {
    "path": "resources/lang/en/auth.php",
    "chars": 276,
    "preview": "<?php\n\nreturn [\n    'login-via' => 'Or log in via',\n\n    'login-failed' => 'Login failed, please try again.',\n\n    'user"
  },
  {
    "path": "resources/views/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "resources/views/components/buttons.blade.php",
    "chars": 1710,
    "preview": "<div\n    x-data=\"{}\"\n    x-load-css=\"[@js(\\Filament\\Support\\Facades\\FilamentAsset::getStyleHref('filament-socialite-styl"
  },
  {
    "path": "routes/web.php",
    "chars": 1799,
    "preview": "<?php\n\nuse DutchCodingCompany\\FilamentSocialite\\Http\\Controllers\\SocialiteLoginController;\nuse DutchCodingCompany\\Filame"
  },
  {
    "path": "src/Events/InvalidState.php",
    "chars": 446,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Events;\n\nuse Illuminate\\Foundation\\Events\\Dispatchable;\nuse Illumi"
  },
  {
    "path": "src/Events/Login.php",
    "chars": 612,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Events;\n\nuse DutchCodingCompany\\FilamentSocialite\\Models\\Contracts"
  },
  {
    "path": "src/Events/Registered.php",
    "chars": 666,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Events;\n\nuse DutchCodingCompany\\FilamentSocialite\\Models\\Contracts"
  },
  {
    "path": "src/Events/RegistrationNotEnabled.php",
    "chars": 589,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Events;\n\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illumi"
  },
  {
    "path": "src/Events/SocialiteUserConnected.php",
    "chars": 678,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Events;\n\nuse DutchCodingCompany\\FilamentSocialite\\Models\\Contracts"
  },
  {
    "path": "src/Events/UserNotAllowed.php",
    "chars": 462,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Events;\n\nuse Illuminate\\Foundation\\Events\\Dispatchable;\nuse Illumi"
  },
  {
    "path": "src/Exceptions/GuardNotStateful.php",
    "chars": 267,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Exceptions;\n\nuse LogicException;\n\nclass GuardNotStateful extends L"
  },
  {
    "path": "src/Exceptions/ImplementationException.php",
    "chars": 127,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Exceptions;\n\nclass ImplementationException extends \\Exception\n{\n  "
  },
  {
    "path": "src/Exceptions/InvalidCallbackPayload.php",
    "chars": 434,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Exceptions;\n\nuse LogicException;\nuse Throwable;\n\nclass InvalidCall"
  },
  {
    "path": "src/Exceptions/ProviderNotConfigured.php",
    "chars": 348,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Exceptions;\n\nuse LogicException;\n\nclass ProviderNotConfigured exte"
  },
  {
    "path": "src/FilamentSocialitePlugin.php",
    "chars": 5378,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite;\n\nuse Closure;\nuse DutchCodingCompany\\FilamentSocialite\\Exceptions"
  },
  {
    "path": "src/FilamentSocialiteServiceProvider.php",
    "chars": 2253,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite;\n\nuse DutchCodingCompany\\FilamentSocialite\\View\\Components\\Buttons"
  },
  {
    "path": "src/Http/Controllers/SocialiteLoginController.php",
    "chars": 7059,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Http\\Controllers;\n\nuse DutchCodingCompany\\FilamentSocialite\\Events"
  },
  {
    "path": "src/Http/Middleware/PanelFromUrlQuery.php",
    "chars": 1390,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Http\\Middleware;\n\nuse Closure;\nuse DutchCodingCompany\\FilamentSoci"
  },
  {
    "path": "src/Models/Contracts/FilamentSocialiteUser.php",
    "chars": 500,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Models\\Contracts;\n\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\n"
  },
  {
    "path": "src/Models/SocialiteUser.php",
    "chars": 1880,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Models;\n\nuse DutchCodingCompany\\FilamentSocialite\\FilamentSocialit"
  },
  {
    "path": "src/Provider.php",
    "chars": 3839,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite;\n\nuse Closure;\nuse DutchCodingCompany\\FilamentSocialite\\Traits\\Can"
  },
  {
    "path": "src/Traits/Callbacks.php",
    "chars": 6743,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Traits;\n\nuse Closure;\nuse DutchCodingCompany\\FilamentSocialite\\Exc"
  },
  {
    "path": "src/Traits/CanBeHidden.php",
    "chars": 757,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Traits;\n\nuse Closure;\n\ntrait CanBeHidden\n{\n    protected bool | Cl"
  },
  {
    "path": "src/Traits/Models.php",
    "chars": 1754,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Traits;\n\nuse DutchCodingCompany\\FilamentSocialite\\Exceptions\\Imple"
  },
  {
    "path": "src/Traits/Routes.php",
    "chars": 909,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Traits;\n\ntrait Routes\n{\n    protected ?string $loginRouteName = nu"
  },
  {
    "path": "src/View/Components/Buttons.php",
    "chars": 1136,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\View\\Components;\n\nuse DutchCodingCompany\\FilamentSocialite\\Filamen"
  },
  {
    "path": "tailwind.config.js",
    "chars": 114,
    "preview": "module.exports = {\n  content: [\"./resources/views/**/*.blade.php\"],\n  corePlugins: {\n    preflight: false,\n  },\n}\n"
  },
  {
    "path": "tests/Fixtures/TestSocialiteUser.php",
    "chars": 602,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests\\Fixtures;\n\nuse Laravel\\Socialite\\Contracts\\User;\n\nclass Test"
  },
  {
    "path": "tests/Fixtures/TestTeam.php",
    "chars": 224,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests\\Fixtures;\n\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nus"
  },
  {
    "path": "tests/Fixtures/TestTenantUser.php",
    "chars": 1079,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests\\Fixtures;\n\nuse Filament\\Models\\Contracts\\HasTenants;\nuse Fil"
  },
  {
    "path": "tests/Fixtures/TestUser.php",
    "chars": 996,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests\\Fixtures;\n\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nus"
  },
  {
    "path": "tests/Fixtures/change_nullable_password_on_users_table.php",
    "chars": 425,
    "preview": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Fa"
  },
  {
    "path": "tests/Fixtures/create_socialite_users_table.php",
    "chars": 691,
    "preview": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Fa"
  },
  {
    "path": "tests/Fixtures/create_team_user_table.php",
    "chars": 520,
    "preview": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Fa"
  },
  {
    "path": "tests/Fixtures/create_teams_table.php",
    "chars": 441,
    "preview": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Fa"
  },
  {
    "path": "tests/SocialiteLoginAuthorizationTest.php",
    "chars": 4389,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests;\n\nuse DutchCodingCompany\\FilamentSocialite\\Events\\UserNotAll"
  },
  {
    "path": "tests/SocialiteLoginTest.php",
    "chars": 6302,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests;\n\nuse Closure;\nuse DutchCodingCompany\\FilamentSocialite\\Even"
  },
  {
    "path": "tests/SocialiteStatelessLoginTest.php",
    "chars": 4757,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests;\n\nuse DutchCodingCompany\\FilamentSocialite\\Events\\InvalidSta"
  },
  {
    "path": "tests/SocialiteTenantLoginTest.php",
    "chars": 2507,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests;\n\nuse DutchCodingCompany\\FilamentSocialite\\FilamentSocialite"
  },
  {
    "path": "tests/TestCase.php",
    "chars": 4808,
    "preview": "<?php\n\nnamespace DutchCodingCompany\\FilamentSocialite\\Tests;\n\nuse DutchCodingCompany\\FilamentSocialite\\FilamentSocialite"
  }
]

About this extraction

This page contains the full source code of the DutchCodingCompany/filament-socialite GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (134.6 KB), approximately 34.6k tokens, and a symbol index with 172 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!