Skip to content

Consent Management Platform (CMP)

The CMP plugin collects user consent (GDPR / IAB TCF) and exposes it through playOps.Cmp(). It shows the consent form when required, reports the outcome, and hands you the stored consent data so you can drive your own ads/attribution decisions.

The current implementation is powered by Google’s User Messaging Platform (UMP). All vendor-specific wiring is handled by the plugin — your code only deals with the vendor-neutral surface below, so the CMP backend can change without touching your integration.

Requirements

1. External Dependency Manager for Unity (EDM4U)

The CMP plugin pulls Android (Maven) and iOS (CocoaPods) native libraries through Google’s External Dependency Manager — Android user-messaging-platform, iOS the Google-Mobile-Ads-SDK pod (which includes UMP). Without it, builds fail at the linker stage with missing UMP symbols.

EDM4U is distributed via OpenUPM. Add the scoped registry and the package to your Packages/manifest.json:

{
"scopedRegistries": [
{
"name": "OpenUPM",
"url": "https://package.openupm.com",
"scopes": ["com.google.external-dependency-manager"]
}
],
"dependencies": {
"com.google.external-dependency-manager": "1.2.187"
}
}

After installing EDM4U, run Assets → External Dependency Manager → Android Resolver → Force Resolve at least once so the Android dependency is written to your gradle template / Assets/Plugins/Android/.

UMP forms are authored server-side and fetched against an AdMob App ID:

  1. An AdMob (or Google Ad Manager) account with the app registered.
  2. A consent message published under the account’s Privacy & messaging tab.
  3. The AdMob App ID present in the build (see Set the AdMob App ID).

3. Order it correctly with ATT and the MMP

On iOS, request App Tracking Transparency before collecting consent, and start the MMP after consent resolves so the first attribution request carries the final consent state. A typical boot sequence is ATT → CMP → MMP:

playOps.AppTracking().RequestAuthorization(_ =>
{
playOps.Cmp().ShowConsentFormIfRequired(_ =>
{
playOps.Mmp()?.Start();
});
});

Install

The Tactile scoped registry must already be configured. Add the plugin to Packages/manifest.json dependencies:

"com.tactilegames.playops-plugins.cmp": "1.0.0"

The plugin transitively pulls its UMP backend (com.tactilegames.google-ump-sdk) and, via EDM4U, the native libraries — no separate install step.

Set the AdMob App ID

UMP fetches the consent message tied to the AdMob App ID baked into the build. The plugin does not set it for you — inject it from a build post-processor on each platform:

  • Android — add the <meta-data> to the generated manifest from an IPostGenerateGradleAndroidProject editor script:
    <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>
  • iOS — set GADApplicationIdentifier in the built Info.plist from a [PostProcessBuild] script (PlistDocument.root.SetString("GADApplicationIdentifier", …)).

Configure

Build a PlayOpsCmpSettings and pass it to the PlayOpsSDK constructor.

using PlayOps;
using PlayOps.Analytics;
using PlayOps.Cmp;
var cmpSettings = new PlayOpsCmpSettings(
tagForUnderAgeOfConsent: false,
debugGeography: CmpDebugGeography.Disabled, // EEA/NotEEA only affect registered test devices
testDeviceHashedIds: null);
var playOps = new PlayOpsSDK(
new PlayOpsSettings(cloudUrl: "https://…", gameSecret: ""),
new AnalyticsSettings(appId: "yourgame"),
cmpSettings);
playOps.Initialize();

Passing the PlayOpsCmpSettings is what enables the plugin and registers the playOps.Cmp() facade.

ShowConsentFormIfRequired is the one-call flow: it refreshes consent info, then shows the form only if the backend reports consent is required. The callback fires once with the final status.

playOps.Cmp().ShowConsentFormIfRequired(result =>
{
if (result.IsSuccess)
{
// consent resolved — safe to start attribution
}
});

Call it at a studio-chosen moment — most games do it at boot, before starting the MMP (see ordering). All callbacks are Action<ConsentResult> and fire on the Unity main thread.

Lower-level calls are also available when you want explicit control:

MemberPurpose
RequestConsentInfoUpdate(cb)Refresh status from the backend. Shows no UI.
ShowConsentForm(cb)Load + show the consent form unconditionally.
Status / IsConsentRequiredCurrent status (Unknown / NotRequired / Required / Obtained).
CanRequestAdsBackend signal that ads may be requested under the current consent.
IsConsentFormAvailableWhether a form has been fetched and is ready to show.

GetConsent() returns a CmpUserConsent read from the standard IAB TCF v2 keys in device storage (Android SharedPreferences, iOS NSUserDefaults):

var consent = playOps.Cmp().GetConsent();
// consent.GdprApplies // 1 when GDPR applies, 0 when not, null when unknown
// consent.TcString // the raw IAB TCF v2 consent string
// consent.PublisherCC // publisher country code
// consent.HasPurposeConsent(3) // 1-based IAB TCF purpose

This is data, not policy — turning purposes into an ads/attribution decision is your game’s call.

Coordinating with the MMP

The CMP plugin has no dependency on the MMP and never calls it — you sequence them. Two patterns:

  1. TCF auto-read (recommended for AppsFlyer). UMP writes the IAB TCF string to device storage when the form resolves; the MMP plugin picks it up automatically when PlayOpsMmpSettings.EnableTcfDataCollection is true. You only control ordering — resolve consent, then start the MMP.
  2. Explicit push. Build an MmpConsent from GetConsent() and pass it to Mmp().UpdateConsent(...) before Start.

See MMP → Advanced: overriding consent for both in detail.

When IsPrivacyOptionsRequired is true, surface a “Manage privacy” entry point so the user can change a prior decision, and present the form with ShowPrivacyOptionsForm:

if (playOps.Cmp().IsPrivacyOptionsRequired)
{
// show a "Manage privacy" button that calls:
playOps.Cmp().ShowPrivacyOptionsForm(_ => { /* re-consent done */ });
}

If you drive the MMP via explicit consent, re-read GetConsent() and call Mmp().UpdateConsent(...) after the form is dismissed.

Testing without a published message

Google’s public sample App ID serves a generic Google test consent form and exercises the full flow on device — in a regulated region (EEA / UK / Switzerland) the form renders and consent resolves. The demo project (PlayOpsDemo) wires the sample IDs via build post-processors as a reference.

Outside a regulated region, force the form for testing by registering your device and setting the debug geography:

var cmpSettings = new PlayOpsCmpSettings(
debugGeography: CmpDebugGeography.EEA,
testDeviceHashedIds: new[] { "YOUR_DEVICE_HASH" });

The device hash is printed in the device log on the first RequestConsentInfoUpdate (look for … addTestDeviceHashedId("…")). DebugGeography has no effect on devices that are not registered as test devices.

Public surface

Reached via playOps.Cmp() once a PlayOpsCmpSettings is passed to the SDK constructor:

namespace PlayOps.Cmp
{
public enum CmpConsentStatus { Unknown, Required, NotRequired, Obtained }
public enum CmpDebugGeography { Disabled = 0, EEA = 1, NotEEA = 2 }
// Outcome of a consent operation, delivered to the callbacks below.
public class ConsentResult
{
bool IsSuccess { get; }
CmpConsentStatus Status { get; }
int ErrorCode { get; } // 0 when IsSuccess
string ErrorMessage { get; } // null when IsSuccess
}
// IAB TCF v2 consent data, returned by GetConsent().
public class CmpUserConsent
{
int? GdprApplies { get; } // 1 = applies, 0 = not, null = unknown
string TcString { get; } // raw IAB TCF v2 consent string
string PurposeConsents { get; } // 1-based purpose bitstring
string PurposeLegitimateInterests { get; }
string PublisherCC { get; } // publisher country code
bool HasPurposeConsent(int purposeId); // 1-based IAB TCF purpose
}
public class PlayOpsCmp
{
CmpConsentStatus Status { get; }
bool IsConsentRequired { get; }
bool IsConsentFormAvailable { get; }
bool CanRequestAds { get; }
bool IsPrivacyOptionsRequired { get; }
void RequestConsentInfoUpdate(Action<ConsentResult> onComplete = null);
void ShowConsentForm(Action<ConsentResult> onComplete = null);
void ShowConsentFormIfRequired(Action<ConsentResult> onComplete = null);
void ShowPrivacyOptionsForm(Action<ConsentResult> onComplete = null);
CmpUserConsent GetConsent();
}
public static class PlayOpsExtension
{
// Registered when a PlayOpsCmpSettings is passed to the PlayOpsSDK constructor.
public static PlayOpsCmp Cmp(this PlayOpsSDK playOps);
}
}