Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1e0e34b
Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0
dependabot[bot] Jan 27, 2025
13e0fde
Merge pull request #5 from DutchCodingCompany/dependabot/github_actio…
github-actions[bot] Jan 27, 2025
dd2042c
Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0
dependabot[bot] May 12, 2025
b51bfee
Merge pull request #7 from DutchCodingCompany/dependabot/github_actio…
github-actions[bot] May 12, 2025
64c694e
Bump stefanzweifel/git-auto-commit-action from 5 to 7
dependabot[bot] Oct 13, 2025
c7053b1
Bump actions/checkout from 4 to 6
dependabot[bot] Nov 24, 2025
c3c0ef0
- Add support for reCAPTCHA v3 Enterprise in README and implementation
Webotvorba Nov 24, 2025
2241948
Add Recaptcha version 2 placeholder element to README
DaniWinter Dec 3, 2025
1a8309c
Bump dependabot/fetch-metadata from 2.4.0 to 2.5.0
dependabot[bot] Jan 12, 2026
24f5a73
Merge pull request #14 from DutchCodingCompany/dependabot/github_acti…
github-actions[bot] Jan 12, 2026
dd5e784
Bump dependabot/fetch-metadata from 2.5.0 to 3.0.0
dependabot[bot] Mar 30, 2026
59ecd24
Merge pull request #13 from DaniWinter/development
bert-w Apr 12, 2026
22192cb
Merge pull request #11 from DutchCodingCompany/dependabot/github_acti…
bert-w Apr 12, 2026
4b9f1ff
Merge pull request #17 from DutchCodingCompany/dependabot/github_acti…
bert-w Apr 12, 2026
52c01af
tweak + expand tests
bert-w Apr 12, 2026
c39b79b
formatting
bert-w Apr 12, 2026
7cbed9d
Merge pull request #12 from Webotvorba/development
bert-w Apr 12, 2026
500cf78
Merge pull request #10 from DutchCodingCompany/dependabot/github_acti…
bert-w Apr 12, 2026
3298320
Add Livewire v4 support + testcases
bert-w Apr 12, 2026
0be7ebc
use phpunit 11
bert-w Apr 12, 2026
4dd7947
add php8.2 exclusion for laravel 13
bert-w Apr 12, 2026
f0b32ce
Merge pull request #18 from DutchCodingCompany/improvement/livewire-v…
bert-w Apr 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

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

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/php-cs-fixer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:

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

Expand All @@ -18,6 +18,6 @@ jobs:
args: --config=.php-cs-fixer.php --allow-risky=yes

- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: Fix styling
2 changes: 1 addition & 1 deletion .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

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

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
30 changes: 19 additions & 11 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,30 @@ on:

jobs:
test:
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [8.3, 8.2]
laravel: [11.*]
stability: [prefer-stable]
testbench: [9.*]

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}
php: ['8.2', '8.3', '8.4', '8.5']
laravel: ['11.*', '12.*', '13.*']
livewire: ['3.*', '4.*']
exclude:
- laravel: '13.*'
php: '8.2'
include:
- laravel: '11.*'
testbench: '9.*'
- laravel: '12.*'
testbench: '10.*'
- laravel: '13.*'
testbench: '11.*'

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

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

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -43,8 +51,8 @@ jobs:

- 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
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "livewire/livewire:${{ matrix.livewire }}" --no-interaction --no-update
composer update --prefer-stable --prefer-dist --no-interaction

- name: List Installed Dependencies
run: composer show -D
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/update-changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:

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

Expand All @@ -21,7 +21,7 @@ jobs:
release-notes: ${{ github.event.release.body }}

- name: Commit updated CHANGELOG
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v7
with:
branch: main
commit_message: Update CHANGELOG
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ version you are going to implement.

This package supports the following versions. Note that each version requires a different sitekey/secretkey pair:

| **Version** | **Docs** | **Notes** |
|----------------------|-------------------------------------------------------------------|-----------------------------|
| **v3** (recommended) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | |
| **v2** | [V2 Docs](https://developers.google.com/recaptcha/docs/display) | |
| **v2 invisible** | [V2 Docs](https://developers.google.com/recaptcha/docs/invisible) | Use `'size' => 'invisible'` |
| **Version** | **Docs** | **Notes** |
|----------------------|-------------------------------------------------------------------|-----------------------------------|
| **v3** (recommended) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | |
| **v3** (enterprise) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | Use `'version' => 'v3-enterprise'` |
| **v2** | [V2 Docs](https://developers.google.com/recaptcha/docs/display) | |
| **v2 invisible** | [V2 Docs](https://developers.google.com/recaptcha/docs/invisible) | Use `'size' => 'invisible'` |

Your options should reside in the `config/services.php` file:

Expand All @@ -38,6 +39,7 @@ Your options should reside in the `config/services.php` file:
'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_KEY'),
'version' => 'v3',
'score' => 0.5, // An integer between 0 and 1, that indicates the minimum score to pass the Captcha challenge.
'endpoint' => 'https://www.google.com/recaptcha/api/siteverify', // For enterprise users, fill in your URL from Google Console.
],
],

Expand Down Expand Up @@ -102,6 +104,10 @@ Secondly, add the new directive `wire:recaptcha` to the form element that you wa
<!-- Add the `wire:recaptcha` Livewire directive -->
<form wire:submit="save" wire:recaptcha>
<!-- The rest of your form -->

<!-- When using version 2, insert the placeholder element -->
<div id="g-recaptcha-element"></div>

<button type="submit">Submit</button>
</form>

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
"license": "MIT",
"require": {
"php": "^8.2",
"livewire/livewire": "^3.0"
"livewire/livewire": "^3.0|^4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.8",
"larastan/larastan": "^2.9",
"orchestra/testbench": "^9.0",
"larastan/larastan": "^2.9|^3.0",
"orchestra/testbench": "^9.0|^10.0",
"phpunit/phpunit": "^11.1"
},
"autoload": {
Expand Down
8 changes: 4 additions & 4 deletions src/LivewireRecaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class LivewireRecaptcha
* @return string
*/
public static function directive(
string $version = null,
string $siteKey = null,
string $theme = null,
string $size = null,
?string $version = null,
?string $siteKey = null,
?string $theme = null,
?string $size = null,
): string {
$version ??= config('services.google.recaptcha.version') ?? 'v3';

Expand Down
6 changes: 5 additions & 1 deletion src/ValidatesRecaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ class ValidatesRecaptcha extends LivewireAttribute
public function __construct(
public ?string $secretKey = null,
public ?float $score = null,
public ?string $endpoint = null,
) {
$this->secretKey ??= config('services.google.recaptcha.secret_key');
$this->score ??= config('services.google.recaptcha.score') ?? 0.5;
$this->endpoint ??= config('services.google.recaptcha.endpoint', 'https://www.google.com/recaptcha/api/siteverify');
}

/**
Expand All @@ -28,8 +30,10 @@ public function __construct(
*/
public function call(array $params, Closure $returnEarly): void
{
assert(is_string($this->endpoint));

if (isset($this->component->gRecaptchaResponse)) {
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
$response = Http::asForm()->post($this->endpoint, [
'secret' => $this->secretKey,
'response' => $this->component->gRecaptchaResponse,
'remoteip' => request()->ip(),
Expand Down
29 changes: 29 additions & 0 deletions src/directive.recaptcha.v3-enterprise.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script>
document.addEventListener('livewire:init', () => {
Livewire.directive('recaptcha', ({ el, directive, component, cleanup }) => {
const submitExpression = (() => {
for (const attr of el.attributes) {
if (attr.name.startsWith('wire:submit')) {
return attr.value;
}
}
})();

const onSubmit = (e) => {
e.preventDefault();
e.stopImmediatePropagation();

grecaptcha.enterprise.ready(async () => {
const token = await grecaptcha.enterprise.execute(@json($siteKey), { action: 'submit' });

component.$wire.$set('gRecaptchaResponse', token).then(() => {
Alpine.evaluate(el, "$wire." + submitExpression, { scope: { $event: e } });
});
});
}

el.addEventListener('submit', onSubmit, { capture: true });
});
});
</script>
<script src="https://www.google.com/recaptcha/enterprise.js?render={{ $siteKey }}"></script>
47 changes: 45 additions & 2 deletions tests/CaptchaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace DutchCodingCompany\LivewireRecaptcha\Tests;

use DutchCodingCompany\LivewireRecaptcha\Tests\Fixtures\MyCustomAttributeComponent;
use DutchCodingCompany\LivewireRecaptcha\Tests\Fixtures\MyTestComponent;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
Expand All @@ -13,7 +14,7 @@ class CaptchaTest extends TestCase
/**
* @param array{0: bool, 1: array{'success': bool}} $captchaResponse
*/
#[DataProvider('provideCaptchaData')]
#[DataProvider('provideDefaultAttributeData')]
public function testInvalidCaptchaResponse(bool $isValid, array $captchaResponse): void
{
Http::fake([
Expand Down Expand Up @@ -45,10 +46,52 @@ public function testInvalidCaptchaResponse(bool $isValid, array $captchaResponse
);
}

/**
* @param array{0: bool, 1: array{'success': bool}} $captchaResponse
*/
#[DataProvider('provideCustomAttributeData')]
public function testCustomAttributeProperties(bool $isValid, array $captchaResponse): void
{
Http::fake([
'https://custom.example.com/verify' => Http::response($captchaResponse),
]);

$testable = Livewire::test(MyCustomAttributeComponent::class)
->set('gRecaptchaResponse', $captcha = 'mygrecaptcharesponse')
->call('save');

if ($isValid) {
$testable->assertHasNoErrors();
} else {
$testable->assertHasErrors([
'gRecaptchaResponse',
]);
}

Http::assertSent(fn (Request $request) => $request->url() === 'https://custom.example.com/verify' &&
$request['secret'] === 'custom-secret-key' &&
$request['response'] === $captcha &&
array_key_exists('remoteip', $request->data())
);
}

/**
* @return array<string, array{0: bool, 1: array{'success': bool}}>
*/
public static function provideCustomAttributeData(): array
{
return [
'valid response' => [true, ['success' => true, 'score' => 0.9]],
'valid response, score at threshold' => [true, ['success' => true, 'score' => 0.7]],
'valid response, score below threshold' => [false, ['success' => true, 'score' => 0.5]],
'invalid response' => [false, ['success' => false]],
];
}

/**
* @return array<string, array{0: bool, 1: array{'success': bool}}>
*/
public static function provideCaptchaData(): array
public static function provideDefaultAttributeData(): array
{
return [
'valid response' => [true, ['success' => true, 'score' => 0.9]],
Expand Down
32 changes: 32 additions & 0 deletions tests/Fixtures/MyCustomAttributeComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace DutchCodingCompany\LivewireRecaptcha\Tests\Fixtures;

use DutchCodingCompany\LivewireRecaptcha\ValidatesRecaptcha;
use Exception;
use Livewire\Component;

class MyCustomAttributeComponent extends Component
{
public string $gRecaptchaResponse;

public function mount(): void
{
//
}

#[ValidatesRecaptcha(
secretKey: 'custom-secret-key',
score: 0.7,
endpoint: 'https://custom.example.com/verify',
)]
public function save(): void
{
//
}

public function render(): string
{
return file_get_contents(__DIR__.'/my-test-component.blade.php') ?: throw new Exception('Failed to load my-test-component.blade.php.');
}
}
Loading