Skip to content

Commit 9e312d6

Browse files
Merge pull request #11 from Laragear/feat/2.x/livewire-filament
[2.x] Adds Livewire and Filament helpers
2 parents cda886e + a66f6c3 commit 9e312d6

19 files changed

Lines changed: 2365 additions & 18 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
/phpunit.xml export-ignore
1111
/tests export-ignore
1212
/.editorconfig export-ignore
13+
/phpstan.neon export-ignore

.github/workflows/php.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ jobs:
6161
- 12.*
6262
- 13.*
6363
dependencies: [ lowest, highest ]
64+
include-filament: [ true ]
6465

6566
steps:
6667
- name: Set up PHP
@@ -73,6 +74,11 @@ jobs:
7374
- name: Checkout code
7475
uses: actions/checkout@v6
7576

77+
# Remove this once Filament PHP v5 gets Laravel 13 compatibility.
78+
- name: Remove Filament for Laravel 13 compatibility
79+
if: matrix.laravel-constraint == '13.*'
80+
run: composer remove "filament/filament" --dev --no-update
81+
7682
- name: Install dependencies
7783
uses: ramsey/composer-install@v3
7884
with:

README.md

Lines changed: 216 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Finally, you can also set a custom callback name to be executed once the script
107107

108108
#### Site Key on JavaScript frontend
109109

110-
If you put the script on the `<head>` part of your HTML view, you may set the `meta` attribute to render a `<meta>` tag alongside the script. This tag will contain your Turnstile site key so your JavaScript frontend can retrieve use it to render the widget.
110+
If you put the script on the `<head>` part of your HTML view, you may set the `meta` attribute to render a `<meta>` tag alongside the script. This tag will contain your Turnstile site-key so your JavaScript frontend can use it to render the widget.
111111

112112
```blade
113113
<!DOCTYPE html>
@@ -161,11 +161,24 @@ Alternatively, you may set a custom name for the tag by setting a value to the `
161161
<meta name="my-custom-key" content="1x00000000000000000000AA" />
162162
```
163163

164+
#### Preconnect
165+
166+
You can also add a [resource hint to Cloudflare servers](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#2-optional-optimize-performance-with-resource-hints) by setting the `preconnect` attribute in the component.
167+
168+
```blade
169+
<x-turnstile::script preconnect />
170+
```
171+
172+
```html
173+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" defer async></script>
174+
<link rel="preconnect" href="https://challenges.cloudflare.com">
175+
```
176+
164177
### Widget
165178

166179
> [!IMPORTANT]
167180
>
168-
> Remember that the Widget Mode is controlled via your [Cloudflare Dashboard](https://dash.cloudflare.com), not here. On development, this is controlled with [testing keys](#testing-keys).
181+
> Remember that the Widget Mode is controlled via your [Cloudflare Dashboard](https://dash.cloudflare.com), not here. In development, this is controlled with [testing keys](#testing-keys).
169182
170183
You can use the `<x-turnstile::widget />` Blade Component to add the Turnstile Widget in your forms. Depending on the Widget Mode, the Widget may render as usual or be invisible at Turnstile discretion.
171184

@@ -440,7 +453,7 @@ use Laragear\Turnstile\Http\Middleware\TurnstileMiddleware;
440453
TurnstileMiddleware::auth('admin');
441454
```
442455

443-
To complement this, you should add the [widget](#widget) to your forms only if user is a guest for the given guards.
456+
To complement this, you should add the [widget](#widget) to your forms only if the user is a guest for the given guards.
444457

445458
```blade
446459
<form id='login' method="POST">
@@ -459,7 +472,7 @@ To complement this, you should add the [widget](#widget) to your forms only if u
459472

460473
#### Middleware accepts failed challenges
461474

462-
You can allow the route to continue even if the challenge failed using the `acceptFailed()` method.
475+
You can allow the route to continue even if the challenge failed, using the `acceptFailed()` method.
463476

464477
```php
465478
use Illuminate\Http\Request;
@@ -564,7 +577,7 @@ $request->validate([
564577
]);
565578
```
566579

567-
#### Rule accepts failed challenges
580+
#### Rule accepts failed challenges
568581

569582
The rule supports not checking if the challenge is successful by setting the `accept-failed` parameter. This can be useful to retrieve the response later and programmatically continue based on the response result through the `sucess()` and `failed()` methods of the `Turnstile` facade.
570583

@@ -840,6 +853,198 @@ return Application::configure(basePath: dirname(__DIR__))
840853
->create();
841854
```
842855
856+
## Livewire Trait
857+
858+
If you're using Liveware, you may use the `Laragear\Turnstile\Livewire\InteractsWithTurnstile` trait in your Livewire pages or components, alongside the [widget](#widget) in your frontend.
859+
860+
The trait overrides the `validate()` method the Component class, and validates the Turnstile Challenge only when all the validation rules pass.
861+
862+
```php
863+
use App\Models\Ball;
864+
use Laragear\Turnstile\Livewire\InteractsWithTurnstile;
865+
use Livewire\Attributes\Rule;
866+
use Livewire\Component;
867+
868+
class MyCustomPage extends Component
869+
{
870+
use InteractsWithTurnstile;
871+
872+
#[Rule('required')]
873+
public $name;
874+
875+
#[Rule('required')]
876+
public $color;
877+
878+
public function save() : Schema
879+
{
880+
$data = $this->validate();
881+
882+
Ball::create($data);
883+
}
884+
}
885+
```
886+
887+
If you're using a custom challenge key in your form, you may override the `turnstileToken()` method to retrieve the value of the token form elsewhere.
888+
889+
```php
890+
public array $data = [];
891+
892+
/**
893+
* Return token for the Turnstile Challenge present in the form.
894+
*/
895+
protected function turnstileToken(): ?string
896+
{
897+
return $this->data['cloudflare-turnstile-token'];
898+
}
899+
```
900+
901+
> [!IMPORTANT]
902+
>
903+
> The Challenge validation does not run when calling `validateOnly()`. In that case, you should [validate manually](#livewire-manual-validation)
904+
905+
### Livewire manual validation
906+
907+
To detach the automatic validation of the Challenge, you can use the `validatesTurnstileAutomatically()` method and return `false`. This way, calling `validate()` in your component won't _consume_ the challenge token.
908+
909+
```php
910+
/**
911+
* If the validation should be run automatically after validation.
912+
*/
913+
protected function validatesTurnstileAutomatically(): bool
914+
{
915+
return false;
916+
}
917+
```
918+
919+
After that, you may call `validateTurnstile()` manually in your component.
920+
921+
```php
922+
public function create()
923+
{
924+
$validated = $this->validate();
925+
926+
// ...
927+
928+
$this->validateTurnstile();
929+
930+
Ball::create($validated);
931+
}
932+
```
933+
934+
### Handling the Turnstile Challenge
935+
936+
You have access to some useful methods to handle if the challenge should be deemed successful or not, and how to handle successes and failures:
937+
938+
* `skipTurnstileValidation()`: Returns if the challenge verification should run or be skipped.
939+
* `handleTurnstileChallengeStatus()`: Returns if the challenge is successful or not.
940+
* `handleSuccessfulTurnstileChallenge()`: Handle a successful Turnstile challenge.
941+
* `handleFailedTurnstileChallenge()`: Handle a failed Turnstile challenge.
942+
943+
For example, for non-admins, you may check if the challenge is successful if it matches the component action:
944+
945+
```php
946+
use Illuminate\Support\Str;
947+
use Laragear\Turnstile\Challenge;
948+
949+
/**
950+
* Check if the Turnstile challenge validation should be skipped if authenticated.
951+
*/
952+
protected function skipTurnstileValidation(): bool
953+
{
954+
// Do not run if the user is an admin.
955+
return auth('admins')->check();
956+
}
957+
958+
/**
959+
* Handles the Turnstile challenge and returns true or false if it has succeeded or failed.
960+
*/
961+
protected function handleTurnstileChallengeStatus(Challenge $challenge): bool
962+
{
963+
return $challenge->successful && $challenge->isAction(Str::snake(static::class));
964+
}
965+
```
966+
967+
## Filament Widget Field
968+
969+
If you're using Filament, you may use the `Laragear\Turnstile\Filament\Forms\TurnstileWidget` field in your forms. It will automatically inject the Turnstile Script in the page and render the Turnstile Widget. The Turnstile Challenge will be validated only on complete form submission.
970+
971+
```php
972+
use Filament\Forms\Components\Textarea;
973+
use Filament\Forms\Components\TextInput;
974+
use Filament\Forms\Concerns\InteractsWithForms;
975+
use Filament\Forms\Contracts\HasForms;
976+
use Filament\Pages\Page;
977+
use Filament\Schemas\Schema;
978+
use Laragear\Turnstile\Filament\Forms\TurnstileWidget;
979+
980+
class ContactPage extends Page implements HasForms
981+
{
982+
use InteractsWithForms;
983+
984+
public function form(Schema $schema) : Schema
985+
{
986+
return $schema->components([
987+
TextInput::make('subject')->required(),
988+
Textarea::make('message')->required(),
989+
// Add the Turnstile Widget
990+
TurnstileWidget::make(),
991+
]);
992+
}
993+
}
994+
```
995+
996+
The Widget includes some convenient methods based on the [Cloudflare Turnstile Widget configuration](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/):
997+
998+
| Method | Description |
999+
|------------------------------------|------------------------------------------------------------------------|
1000+
| `normal()` | Sets the size of the widget to normal. |
1001+
| `flexible()` | Sets the size of the widget to flexible. |
1002+
| `compact()` | Sets the size of the widget to compact. |
1003+
| `system()` | Sets the theme of the widget to system/browser default. |
1004+
| `light()` | Sets the theme of the widget to light. |
1005+
| `dark()` | Sets the theme of the widget to dark. |
1006+
| `appearanceAlways()` | Sets the appearance of the widget to always appear. |
1007+
| `appearanceExecute()` | Sets the appearance of the widget to appear on manual execution. |
1008+
| `appearanceInteractionOnly()` | Sets the appearance of the widget to appear only when required. |
1009+
| `executionRender()` | Sets the execution of the widget challenge as it renders. |
1010+
| `executionExecute()` | Sets the execution of the widget challenge on manual execution. |
1011+
| `languageAuto()` | Sets the language of the widget to browser locale. |
1012+
| `language(string $lang)` | Sets the language of the widget to given locale. |
1013+
| `languageApp()` | Sets the language of the widget to application locale. |
1014+
| `tabindex(int $tabindex)` | Sets the tab order of the widget to one set. |
1015+
| `callback(string $functionName)` | Adds an additional function names to execute on successful challenges. |
1016+
| `actionName(string $functionName)` | Sets the action name for the challenge. |
1017+
| `implicit(bool $condition = true)` | Makes the widget be rendered automatically by the script. |
1018+
1019+
> [!IMPORTANT]
1020+
>
1021+
> The Filament Turnstile Widget is rendered **implicitly** by default, meaning its rendering is handled internally by AlpineJS. This makes all the options to be passed as a JavaScript object. You can use **explicit** rendering, but you may have problems if Livewire or Filament are configured to run in [SPA mode](https://filamentphp.com/docs/5.x/panel-configuration#spa-mode) or [use islands](https://livewire.laravel.com/docs/4.x/islands).
1022+
1023+
### Script injection
1024+
1025+
You don't need to [manually inject the script](#script) in your app frontend, the Filament Widget Field does it automatically for you. Alternatively, disable the injection using `withoutScript()`.
1026+
1027+
```php
1028+
use Laragear\Turnstile\Filament\Forms\TurnstileWidget;
1029+
1030+
TurnstileWidget::make()->withoutScript();
1031+
```
1032+
1033+
You may also pass the stack where the script should pushed if it's not `scripts`, and additional attributes to pass to the widget, using `withScript()`.
1034+
1035+
```php
1036+
use Laragear\Turnstile\Filament\Forms\TurnstileWidget;
1037+
1038+
TurnstileWidget::make()->withScript(
1039+
stack: 'head-scripts',
1040+
attributes: ['meta' => true]
1041+
);
1042+
```
1043+
1044+
> [!NOTE]
1045+
>
1046+
> If you use `implicit()` rendering on the widget, the widget will automatically remove the [`explicit` flag](#widget) from the script, which will make all widgets render as soon as possible.
1047+
8431048
## Advanced configuration
8441049

8451050
Laragear Turnstile is intended to work out-of-the-box, but you can publish the configuration file for fine-tuning the Challenge verification.
@@ -856,9 +1061,12 @@ You will get a config file with this array:
8561061
return [
8571062
'env' => env('TURNSTILE_ENV', env('APP_ENV')),
8581063
'key' => \Laragear\Turnstile\Turnstile::KEY,
859-
'client' => [
860-
\GuzzleHttp\RequestOptions::VERSION => 1.1,
861-
],
1064+
'client' => array_merge_recursive([
1065+
\GuzzleHttp\RequestOptions::VERSION => 2.0,
1066+
],
1067+
defined('CURL_VERSION_HTTP3') && curl_version()['features'] & CURL_VERSION_HTTP3
1068+
? ['curl' => [CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_3]] : [],
1069+
),
8621070
'site_key' => env('TURNSTILE_SITE_KEY'),
8631071
'secret_key' => env('TURNSTILE_SECRET_KEY'),
8641072
'interstitial' => [

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"illuminate/session": "12.*|13.*"
3636
},
3737
"require-dev": {
38+
"filament/filament": "5.*",
39+
"livewire/livewire": "4.*",
3840
"orchestra/testbench": "10.*|11.*"
3941
},
4042
"autoload": {

lang/en/notification.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
return [
4+
'failed' => [
5+
'title' => 'The Turnstile Challenge is invalid',
6+
'body' => 'Please try again or refresh the page.',
7+
],
8+
];

lang/es/interstitial.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
return [
4+
'title' => 'Comprobando la conexión segura',
5+
'description' => 'Su navegador será redirigido automáticamente',
6+
];

lang/es/notification.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
return [
3+
'failed' => [
4+
'title' => 'El desafío de Turnstile no es válido',
5+
'body' => 'Por favor, inténtalo de nuevo o recarga la página.',
6+
],
7+
];

lang/es/validation.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
return [
4+
'invalid' => 'El desafío de Turnstile es inválido, ausente o falló.',
5+
];

phpstan.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
parameters:
2+
ignoreErrors:
3+
- '#Trait Laragear\\Turnstile\\Livewire\\InteractsWithTurnstile is used zero times and is not analysed\.#'

0 commit comments

Comments
 (0)