Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 79 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,25 @@ class CookiesServiceProvider extends ServiceProvider
->session()
->csrf();

// Register all Analytics cookies at once using one single shorthand method:
// Register all Google Analytics cookies at once using one single shorthand method:
// Don't forget to change the id an anonymizeIp to point to the good location
Cookies::analytics()
->google(
id: config('cookieconsent.google_analytics.id')
anonymizeIp: config('cookieconsent.google_analytics.anonymize_ip')
);
->googleAnalytics(
id: config('your_config.google_analytics.id'),
anonymizeIp: config('your_config.google_analytics.anonymize_ip')
);

// Using Google Tag Manager? Use this method instead of the Google Analytics one;
Cookies::analytics()
->googleTagManager(
id: config('your_config.google_tag_manager.id'),
config: [
'ad_user_data',
'ad_personalization',
'ad_storage',
'analytics_storage',
]
);

// Register custom cookies under the pre-existing "optional" category:
Cookies::optional()
Expand All @@ -129,6 +142,8 @@ class CookiesServiceProvider extends ServiceProvider

More details on the available [cookie registration](#registering-cookies) methods below.

More details on the [Google Analytics and Google Tag Manager](#analytics-cookies-custom-methods) methods below.

Then, let's add consent scripts and modals to the application's views using the following blade directives:

- `@cookieconsentscripts`: used to add the package's default JavaScript and any third-party scripts you need to get the end-user's consent for.
Expand Down Expand Up @@ -413,6 +428,65 @@ Most of the displayed strings are defined in the `cookieConsent::cookies` transl

If not already published, you can edit or fill the translation files using `php artisan vendor:publish --tag=laravel-cookie-consent-lang`, this will copy our translation files to your app's `vendor/cookieConsent` "lang" path.

## Analytics cookies custom methods

### Google Analytics (GA)

Register all Google Analytics cookies at once using the shorthand method:

```php
Cookies::analytics()
->googleAnalytics(
id: config('services.google_analytics.id'),
anonymizeIp: config('services.google_analytics.anonymize_ip', true)
);
```

### Google Tag Manager (GTM)

For Google Tag Manager with consent mode v2, use this method instead:

```php
Cookies::analytics()
->googleTagManager(
id: id: config('services.google_tag_manager.id'), // Your Google Tag Manager Container ID
config: [
'ad_storage', // Enables storage for advertising
'ad_user_data', // Enables sending user data for advertising
'ad_personalization', // Enables personalized advertising
'analytics_storage', // Enables storage for analytics
]
);
```

You can find all the available consent types on [the Google Tag Manager documentation](https://support.google.com/tagmanager/answer/10718549?hl=en)

**Config file example (`config/services.php`):**
```php
return [
'google_analytics' => [
'id' => env('GOOGLE_ANALYTICS_ID'),
'anonymize_ip' => env('GOOGLE_ANALYTICS_ANONYMIZE_IP', true),
],

'google_tag_manager' => [
'id' => env('GOOGLE_TAG_MANAGER_ID'),
],
];
```

**Environment variables (`.env`):**
```env
GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
GOOGLE_ANALYTICS_ANONYMIZE_IP=true

GOOGLE_TAG_MANAGER_ID=GTM-XXXXXXX
```

**Note:** When using Google Tag Manager, the consent mode is automatically configured. Users must accept the analytics cookies before GTM scripts are loaded. The default consent state is set to "denied" for all specified consent types until the user gives explicit consent.

> **Important:** Don't use both `googleAnalytics()` and `googleTagManager()` methods together. Choose one based on your tracking setup.

## A few useful tips

> **Disclaimer**: We are not lawyers. Always check with your legal partners which rules may apply to your project.
Expand Down
13 changes: 13 additions & 0 deletions config/cookieconsent.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,17 @@
'anonymize_ip' => env('GOOGLE_ANALYTICS_ANONYMIZE_IP', true)
],

/* Google Tag Manager configuration
|--------------------------------------------------------------------------
|
| If you use Google Tag Manager, you can configure the package to automatically
| load the Google Tag Manager script when the user gives his consent.
|
| The ID parameter is required and represents your Google Tag Manager ID.
|
*/
'google_tag_manager' => [
'id' => env('GOOGLE_TAG_MANAGER_ID'),
],

];
70 changes: 51 additions & 19 deletions src/AnalyticCookiesCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,40 @@
class AnalyticCookiesCategory extends CookiesCategory
{
const GOOGLE_ANALYTICS = 'ga';
const GOOGLE_TAG_MANAGER = 'gtm';

protected function getGroupCookies($group, $name, $id)
{
$key = str_starts_with($id, 'G-') ? substr($id, 2) : $id;

return $group->name($name)
->cookie(fn(Cookie $cookie) => $cookie->name('_ga')
->duration(2 * 365 * 24 * 60)
->description(__('cookieConsent::cookies.defaults._ga'))
)
->cookie(fn(Cookie $cookie) => $cookie->name('_ga_' . strtoupper($key))
->duration(2 * 365 * 24 * 60)
->description(__('cookieConsent::cookies.defaults._ga_ID'))
)
->cookie(fn(Cookie $cookie) => $cookie->name('_gid')
->duration(24 * 60)
->description(__('cookieConsent::cookies.defaults._gid'))
)
->cookie(fn(Cookie $cookie) => $cookie->name('_gat')
->duration(1)
->description(__('cookieConsent::cookies.defaults._gat'))
);
}

/**
* Define Google Analytics cookies all at once.
*/
public function google(string $id, bool $anonymizeIp = true): static
public function googleAnalytics(string $id, bool $anonymizeIp = true): static
{
$this->group(function (CookiesGroup $group) use ($anonymizeIp, $id) {
$key = str_starts_with($id, 'G-') ? substr($id, 2) : $id;
$anonymizeIp = $anonymizeIp === true ? 'true' : 'false';

$group->name(static::GOOGLE_ANALYTICS)
->cookie(fn(Cookie $cookie) => $cookie->name('_ga')
->duration(2 * 365 * 24 * 60)
->description(__('cookieConsent::cookies.defaults._ga'))
)
->cookie(fn(Cookie $cookie) => $cookie->name('_ga_' . strtoupper($key))
->duration(2 * 365 * 24 * 60)
->description(__('cookieConsent::cookies.defaults._ga_ID'))
)
->cookie(fn(Cookie $cookie) => $cookie->name('_gid')
->duration(24 * 60)
->description(__('cookieConsent::cookies.defaults._gid'))
)
->cookie(fn(Cookie $cookie) => $cookie->name('_gat')
->duration(1)
->description(__('cookieConsent::cookies.defaults._gat'))
)
$this->getGroupCookies(group: $group, name: static::GOOGLE_ANALYTICS, id: $id)
->accepted(fn(Consent $consent) => $consent
->script('<script async src="https://www.googletagmanager.com/gtag/js?id=' . $id . '"></script>')
->script(
Expand All @@ -40,6 +47,31 @@ public function google(string $id, bool $anonymizeIp = true): static
);
});

return $this;
}

public function googleTagManager(string $id, $config): static
{
GoogleTagManagerConfig::enable($config);

$consentSettings = array_fill_keys($config, 'granted');

$consentJson = json_encode($consentSettings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

$this->group(function (CookiesGroup $group) use ($consentJson, $id) {
$this->getGroupCookies(group: $group, name: static::GOOGLE_TAG_MANAGER, id: $id)
->accepted(fn(Consent $consent) => $consent
->script('<script async src="https://www.googletagmanager.com/gtag/js?id=' . $id . '"></script>')
->script(
'<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({\'gtm.start\':new Date().getTime(),event:\'gtm.js\'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!=\'dataLayer\'?\'&l=\'+l:\'\';j.async=true;j.src=\'https://www.googletagmanager.com/gtm.js?id=\'+i+dl;f.parentNode.insertBefore(j,f);})(window,document,\'script\',\'dataLayer\',\''. $id .'\');</script>'
)
->script(
'<script>function loadGoogleTagManagerScript() {window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);} gtag(\'consent\', \'update\', ' . $consentJson . ');} loadGoogleTagManagerScript()</script>'
)
);
});


return $this;
}
}
44 changes: 43 additions & 1 deletion src/CookiesManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Whitecube\LaravelCookieConsent;

use Illuminate\Database\Eloquent\Casts\Json;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie as CookieFacade;
use Symfony\Component\HttpFoundation\Cookie as CookieComponent;
Expand All @@ -18,6 +19,12 @@ class CookiesManager
*/
protected ?array $preferences = null;

/**
* Google Tag Manager configuration
*/
protected bool $gtmEnabled = false;
protected array $gtmConfig = [];

/**
* Create a new Service Manager instance.
*/
Expand Down Expand Up @@ -182,7 +189,11 @@ public function renderScripts(bool $withDefault = true): string

public function getNoticeScripts(bool $withDefault): string
{
return $withDefault ? $this->getDefaultScriptTag() : '';
$output = $withDefault ? $this->getDefaultScriptTag() : '';

$output .= $this->getGTMScript();

return $output;
}

protected function getConsentedScripts(bool $withDefault): string
Expand All @@ -205,6 +216,37 @@ protected function getDefaultScriptTag(): string
. '></script>';
}

/**
* Define default Google Tag Manager script tag;
*/
private function getGTMScript(): string
{
if (GoogleTagManagerConfig::isEnabled()) {
$this->gtmEnabled = true;
$this->gtmConfig = GoogleTagManagerConfig::getConfig();
}

if (! $this->gtmEnabled){
return '';
}

$defaultSettings = $this->gtmConfig;

$defaultSettings = array_fill_keys($defaultSettings, 'denied');
$defaultSettings['wait_for_update'] = 500;

$defaultJson = json_encode($defaultSettings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

return '<script type="module" defer>
window.dataLayer = window.dataLayer || []; function gtag() {
dataLayer.push(arguments);
}

gtag(\'consent\', \'default\', ' . $defaultJson . ');
dataLayer.push({\'gtm.start\': new Date().getTime(), \'event\': \'gtm.js\'});
</script> ';
}

/**
* Output the consent alert/modal for current consent state.
*/
Expand Down
31 changes: 31 additions & 0 deletions src/GoogleTagManagerConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Whitecube\LaravelCookieConsent;

class GoogleTagManagerConfig
{
protected static bool $enabled = false;
protected static array $config = [];

public static function enable(array $config): void
{
static::$enabled = true;
static::$config = $config;
}

public static function isEnabled(): bool
{
return static::$enabled;
}

public static function getConfig(): array
{
return static::$config;
}

public static function reset(): void
{
static::$enabled = false;
static::$config = [];
}
}
20 changes: 16 additions & 4 deletions stubs/CookiesServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,22 @@ protected function registerCookies(): void
->csrf();

// Register all Analytics cookies at once using one single shorthand method:
// Don't forget to change the config location to match your config file
// Cookies::analytics()
// ->google(
// id: config('cookieconsent.google_analytics.id'),
// anonymizeIp: config('cookieconsent.google_analytics.anonymize_ip')
// ->googleAnalytics(
// id: config('your_config.google_analytics.id'),
// anonymizeIp: config('your_config.google_analytics.anonymize_ip')
// );

// Cookies::analytics()
// ->googleTagManager(
// id: config('your_config.google_tag_manager.id'),
// config: [
// 'ad_user_data',
// 'ad_personalization',
// 'ad_storage',
// 'analytics_storage',
// ]
// );

// Register custom cookies under the pre-existing "optional" category:
Expand All @@ -31,4 +43,4 @@ protected function registerCookies(): void
// ->duration(120)
// ->accepted(fn(Consent $consent, MyDarkmode $darkmode) => $consent->cookie(value: $darkmode->getDefaultValue()));
}
}
}
2 changes: 1 addition & 1 deletion tests/Unit/AnalyticCookiesCategoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
it('can register Google Analytics configuration', function () {
$category = new AnalyticCookiesCategory('foo');

expect($category->google('g-foo'))->toBe($category);
expect($category->googleAnalytics('g-foo'))->toBe($category);
expect($group = ($category->getDefined()[0] ?? null))->toBeInstanceOf(CookiesGroup::class);

expect($group->hasConsentCallback())->toBeTrue();
Expand Down