Skip to main content

Implementing Google UMP SDK with Unity Ads for GDPR Compliance

 Receiving a policy violation notification from Google Play Console regarding "Invalid Privacy Policy" or "Missing CMP" is a critical blocker for mobile publishers. Google now strictly enforces the use of a certified Consent Management Platform (CMP) compatible with TCF v2.2.

If you initialize Unity Ads (or any ad network) before resolving the user's consent status via Google's User Messaging Platform (UMP), you are technically tracking users without permission. This leads to app rejections and potential account bans.

This guide details a robust, architectural approach to strictly order your initialization logic in C#, ensuring UMP resolves fully before Unity Ads ever touches the network.

The Root Cause: Race Conditions in Initialization

The core issue stems from the asynchronous nature of Unity's Start() lifecycle and SDK initializations.

In a standard implementation, developers often place Advertisement.Initialize() and MobileAds.Initialize() in the Start() method of a loading script. This creates a race condition. If Unity Ads initializes and requests an ad fill before the UMP SDK has established a consent status, the ad network may drop a tracking cookie or collect a Device ID.

Under GDPR and TCF v2.2, this "prior tracking" is a violation. You must implement Sequential Initialization. The application flow must be blocked until the Consent Form is either dismissed, accepted, or deemed unnecessary by the geo-location logic.

Prerequisites

Ensure you have the following packages installed in your Unity project:

  1. Google Mobile Ads SDK (v9.0.0 or higher)
  2. Unity Ads SDK (via Package Manager or Asset Store)

The Solution: A Dedicated Consent Manager

We will create a ConsentManager class. This class is responsible for determining requirements, showing the form, and—crucially—invoking a callback only when it is safe to proceed with ad initialization.

1. The C# Implementation

Create a script named ConsentManager.cs. This script handles the UMP lifecycle and bridges the consent data to Unity Ads.

using UnityEngine;
using GoogleMobileAds.Ump.Api;
using UnityEngine.Advertisements;
using System;

public class ConsentManager : MonoBehaviour
{
    // Singleton instance for easy access
    public static ConsentManager Instance { get; private set; }

    [Header("Configuration")]
    [Tooltip("Enable this for testing in non-GDPR regions (e.g., Simulator as EEA)")]
    public bool testMode = false;
    public string testDeviceId = "YOUR_TEST_DEVICE_ID"; // Check logs for this ID

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// Entry point: Checks consent requirements and shows form if necessary.
    /// </summary>
    /// <param name="onComplete">Callback fired when Ad init can proceed.</param>
    public void GatherConsent(Action onComplete)
    {
        var requestParameters = new ConsentRequestParameters
        {
            TagForUnderAgeOfConsent = false,
        };

        // Configure Test Device settings for development
        if (testMode)
        {
            var debugSettings = new ConsentDebugSettings
            {
                DebugGeography = DebugGeography.EEA,
                TestDeviceHashedIds = new System.Collections.Generic.List<string> 
                {
                    testDeviceId 
                }
            };
            requestParameters.ConsentDebugSettings = debugSettings;
        }

        // 1. Request Consent Information Update
        ConsentInformation.Update(requestParameters, (FormError updateError) =>
        {
            if (updateError != null)
            {
                Debug.LogError($"UMP Update Error: {updateError.Message}");
                // Even on error, we usually proceed to attempt init 
                // to avoid blocking the app entirely, but treat as no consent.
                onComplete?.Invoke();
                return;
            }

            // 2. Load and Show Form if required
            LoadAndShowConsentFormIfRequired(onComplete);
        });
    }

    private void LoadAndShowConsentFormIfRequired(Action onComplete)
    {
        // If the status is 'Required', the SDK handles showing the form.
        // If 'NotRequired' or 'Obtained', it invokes callback immediately.
        ConsentForm.LoadAndShowConsentFormIfRequired((FormError showError) =>
        {
            if (showError != null)
            {
                Debug.LogError($"UMP Show Error: {showError.Message}");
                onComplete?.Invoke();
                return;
            }

            // 3. Form handling complete. Forward consent to Unity Ads.
            ForwardConsentToUnityAds();

            // 4. Safe to initialize Ads
            onComplete?.Invoke();
        });
    }

    private void ForwardConsentToUnityAds()
    {
        // Check standard Google UMP consent status
        if (ConsentInformation.CanRequestAds())
        {
            // User consented or is outside GDPR region
            MetaData gdprMetaData = new MetaData("gdpr");
            gdprMetaData.Set("consent", "true");
            Advertisement.SetMetaData(gdprMetaData);
            Debug.Log("GDPR Consent: TRUE sent to Unity Ads");
        }
        else
        {
            // User declined consent
            MetaData gdprMetaData = new MetaData("gdpr");
            gdprMetaData.Set("consent", "false");
            Advertisement.SetMetaData(gdprMetaData);
            Debug.Log("GDPR Consent: FALSE sent to Unity Ads");
        }

        // CCPA Handling (Privacy Laws in California)
        // If privacy options are required, it usually implies CCPA applicability
        if (ConsentInformation.PrivacyOptionsRequirementStatus == PrivacyOptionsRequirementStatus.Required)
        {
             // For strict CCPA compliance, you check specific IAB strings. 
             // However, Unity Ads simply requires the boolean flag for "do not sell".
             // This is a simplified fallback.
             MetaData ccpaMetaData = new MetaData("privacy");
             ccpaMetaData.Set("consent", "true");
             Advertisement.SetMetaData(ccpaMetaData);
        }
    }
    
    // Helper to reset consent state for testing (attach to a UI button)
    public void ResetConsentState()
    {
        ConsentInformation.Reset();
        Debug.Log("Consent state reset.");
    }
}

2. The Initialization Orchestrator

Do not initialize Unity Ads in Start. Instead, create a bootstrap script that calls our ConsentManager.

using UnityEngine;
using UnityEngine.Advertisements;

public class AppInitializer : MonoBehaviour
{
    [Header("Unity Ads Config")]
    public string androidGameId = "1234567";
    public string iosGameId = "1234568";
    public bool testMode = true;

    private void Start()
    {
        // Block interaction or show loading spinner here
        Debug.Log("Starting Initialization Sequence...");

        ConsentManager.Instance.GatherConsent(OnConsentProcessComplete);
    }

    private void OnConsentProcessComplete()
    {
        Debug.Log("Consent flow finished. Initializing Unity Ads...");
        InitializeUnityAds();
    }

    private void InitializeUnityAds()
    {
        string gameId = (Application.platform == RuntimePlatform.IPhonePlayer)
            ? iosGameId
            : androidGameId;

        if (!Advertisement.isInitialized && Advertisement.isSupported)
        {
            Advertisement.Initialize(gameId, testMode, new AdsInitListener());
        }
    }

    // Listener for Unity Ads Initialization
    private class AdsInitListener : IUnityAdsInitializationListener
    {
        public void OnInitializationComplete()
        {
            Debug.Log("Unity Ads Initialized Successfully.");
            // Load your Banner/Interstitial here
        }

        public void OnInitializationFailed(UnityAdsInitializationError error, string message)
        {
            Debug.LogError($"Unity Ads Init Failed: {error} - {message}");
        }
    }
}

Deep Dive: How This Architecture Works

The Update Request

The call to ConsentInformation.Update connects to Google's servers. It sends basic device info to determine if the user is in a regulated jurisdiction (EEA or UK). If the user is in the US, it checks for state-level regulations. This step is mandatory; you cannot show a form without knowing if one is required.

LoadAndShowConsentFormIfRequired

This is a helper method introduced in recent UMP SDK versions. It simplifies the logic significantly.

  • Case A (No Consent Needed): The user is not in the EEA. The callback fires immediately.
  • Case B (Consent Needed, No Choice Made): The SDK loads the UI overlay, presents it, waits for user input, saves the preference to SharedPreferences/UserDefaults, and then fires the callback.
  • Case C (Consent Needed, Already Chosen): The SDK detects a valid TCF string on the device and fires the callback immediately without showing UI.

Bridging to Unity Ads

Unity Ads does not automatically read the TCF v2.2 string generated by Google UMP (unlike AdMob, which reads it automatically).

You must manually set the Unity Ads MetaData. By calling Advertisement.SetMetaData, we explicitly tell Unity's backend whether we have permission to serve personalized ads. We determine this using ConsentInformation.CanRequestAds(). This method returns true if the user consented OR if the user is in a region where consent isn't required.

Common Pitfalls and Edge Cases

1. Testing Geography

You cannot test the consent form if you are physically located outside the EEA. The Fix: Use the ConsentDebugSettings in the code above. Set DebugGeography = DebugGeography.EEA and ensure your testDeviceId (found in the Logcat/Xcode console) is added to the list.

2. The Privacy Options Button

GDPR requirements dictate that users must be able to revoke or modify consent at any time. You must add a button in your game's settings menu labeled "Privacy Settings". Implementation: Check ConsentInformation.PrivacyOptionsRequirementStatus. If it is Required, enable your settings button. When clicked, call ConsentForm.ShowPrivacyOptionsForm.

3. iOS App Tracking Transparency (ATT)

On iOS 14+, you also need the IDFA permission. Google UMP can handle the ATT alert triggering after the GDPR consent. Ensure you have configured the IDFA message inside your AdMob Privacy & Messaging console. The LoadAndShowConsentFormIfRequired chain handles the sequencing of the GDPR form followed by the iOS system alert automatically.

4. Mediation Complexity

If you use Unity Ads as a mediation source inside AdMob (using the Unity Ads Adapter), the adapter usually handles the consent forwarding automatically via TCF strings. However, if you are using Unity Ads as a standalone SDK (LevelPlay or direct), the manual MetaData passing demonstrated above is mandatory.

Conclusion

Compliance is not just about avoiding fines; it's about app stability and user trust. By decoupling your application start logic from your ad initialization logic, you ensure that every network call made by Unity Ads is legally compliant.

Implement the ConsentManager as a strict gatekeeper. If the consent flow doesn't finish, the ads don't load. This binary approach satisfies Google's rigorous policy enforcement and keeps your developer account safe.