Consent Plugin for NativePHP Mobile#
Apple's App Tracking Transparency prompt and Google's consent form for GDPR and US state privacy laws, in your NativePHP app.
See it in action#
Overview#
If your app shows ads, two questions have to be asked before the first ad loads:
- Google's consent form (Google's User Messaging Platform). Google requires it for ads served to people in the EEA, the UK and Switzerland, and it covers US state privacy laws too. You design the form in AdMob; this plugin shows it to the people who need it, and only once.
- Apple's "Allow this app to track your activity?" prompt. Apple requires it before an iOS app uses the advertising ID.
The plugin:
- asks in the order Google recommends, and tells you when ads may be requested (
Consent::canRequestAds()); - adds the tracking wording to iOS and your AdMob app ID to both platforms at build time, unless an ads plugin has already added the ID (it never adds a second, which would break the build);
- shows the "privacy settings" form people must be able to reopen in the EEA.
It works with any ads plugin. It doesn't load ads itself.
Requirements: NativePHP Mobile 4.x, Laravel 11–13, PHP 8.3+, iOS 18+, Android 10+, and an AdMob account with a consent message published (AdMob → Privacy & messaging).
Installation#
composer require firsthand/nativephp-consent
Don't forget to register the plugin, and publish the config:
php artisan native:plugin:register firsthand/nativephp-consentphp artisan vendor:publish --tag=consent-config
Set your wording and AdMob app IDs. Leave the IDs out if your ads plugin already sets them.
CONSENT_TRACKING_REASON="Lets us show ads that fit what you cook."CONSENT_ADMOB_APP_ID_IOS=ca-app-pub-0000000000000000~0000000000CONSENT_ADMOB_APP_ID_ANDROID=ca-app-pub-0000000000000000~0000000000
Then rebuild your app.
Usage#
PHP (Super Native / Livewire / Blade)#
On every launch:
use Firsthand\Consent\Facades\Consent; Consent::gather(); // shows Google's form only to people who still need to answer
When ConsentUpdated arrives:
if (Consent::canRequestAds()) { // load your ads}Consent::requestTracking(); // iOS: Apple's prompt, after Google's form
Where your app keeps its settings:
if (Consent::status()['privacy_options_required'] ?? false) { Consent::showPrivacyOptions(); // behind a "Privacy settings" button}
JavaScript (Vue / React / Inertia)#
import { gather, canRequestAds, requestTracking, onConsentUpdated } from '@firsthand/nativephp-consent'; onConsentUpdated(async ({ canRequestAds }) => { if (canRequestAds) loadAds(); await requestTracking();});await gather();
TypeScript types are included.
Events#
ConsentUpdated#
Firsthand\Consent\Events\ConsentUpdated, fired when gather() or showPrivacyOptions() finishes, whether or not a form was shown.
| Property | Type | Description |
|---|---|---|
status |
string |
unknown, required, not_required or obtained |
can_request_ads |
bool |
Whether ads may be requested now |
privacy_options_required |
bool |
Whether you must offer a "Privacy settings" entry point |
message |
?string |
Why Google couldn't be reached or the form failed; earlier answers still stand |
TrackingAnswered#
Firsthand\Consent\Events\TrackingAnswered, fired when Apple's prompt closes. iOS asks only once, so later calls answer immediately.
| Property | Type | Description |
|---|---|---|
status |
string |
authorized, denied, restricted, not_determined, or not_applicable (Android) |
use Firsthand\Consent\Events\ConsentUpdated;use Native\Mobile\Attributes\On; #[On(ConsentUpdated::class)] // #[OnNative(...)] in a Livewire web-view pagepublic function consent(bool $can_request_ads): void { … }
Methods#
| Method | Returns |
|---|---|
gather(?string $debugGeography = null, array $testDeviceIds = []) |
bool; the outcome arrives as ConsentUpdated |
status() |
['status', 'can_request_ads', 'privacy_options_required'], or null outside the app |
canRequestAds() |
bool |
showPrivacyOptions() |
bool; the outcome arrives as ConsentUpdated |
trackingStatus() |
authorized, denied, restricted, not_determined, not_applicable |
requestTracking() |
bool; the answer arrives as TrackingAnswered |
reset() |
forgets the stored consent. For testing only |
Testing the form from anywhere: Consent::gather('eea') pretends the device is in the EEA. On a real device, also pass the test device ID that Google prints in the device log. Never ship either.
Platform behavior#
- iOS shows Apple's prompt once per install. After that,
requestTracking()returns the stored answer. People change it in Settings → Privacy & Security → Tracking. - Android has no tracking prompt:
trackingStatus()isnot_applicableandrequestTracking()answers at once. - Google's form only appears where it is required, for people who haven't answered. Elsewhere
gather()finishes withnot_requiredandcan_request_ads: true. - If Google can't be reached,
ConsentUpdatedcarries amessage, and the last stored answer still applies.
Testing#
Script the bridge with NativePHP's testing suite:
use Firsthand\Consent\Events\ConsentUpdated;use Native\Mobile\Testing\Native; it('loads ads once consent is in', function () { Native::fakeBridge() ->respondTo('Consent.Gather', ['started' => true]) ->respondTo('Consent.Status', ['status' => 'obtained', 'can_request_ads' => true]); Native::test(HomeScreen::class) ->emitNative(ConsentUpdated::class, ['status' => 'obtained', 'can_request_ads' => true]) ->assertSee('Sponsored');});
Troubleshooting#
| What you see | Why |
|---|---|
| The build warns "no AdMob app ID" | Set CONSENT_ADMOB_APP_ID_IOS / _ANDROID, or install an ads plugin that adds it |
message: "Publisher misconfiguration" |
No consent message is published for this app in AdMob → Privacy & messaging |
| The form never shows while testing | You're outside the EEA and haven't answered: use gather('eea'), and reset() to see it again |
| iOS: no tracking prompt | iOS already has an answer, or tracking is turned off for all apps in Settings |
| A native screen shows no text | Native screens need NativePHP's UI package, nativephp/mobile-ui |
License#
One purchase covers unlimited apps. See the LICENSE file in the package.