How to implement UMP SDK correctly for eu consent?

8.7k views Asked by At

I have lots of confusion regarding the implementation of the UMP SDK. I have not found much information or a complete tutorial other than google. I am following this and this but unable to understand bellow issues:

  1. Is it required to call MobileAds.initialize() after getting the requesting consent? If so, then where should it be called? It might be called after obtaining the consent:

    public void onConsentFormLoadSuccess(ConsentForm consentForm) {
        if(consentInformation.getConsentStatus() == ConsentInformation.ConsentStatus.OBTAINED) {
    }
    }
    
  2. How would I check if a user is not from EEA? I wanted to request consent or initialize mobile ads based on user location. In Consent SDK, there is a method isRequestLocationInEeaOrUnknown(), but this SDK is deprecated. I have not found something similar in UMP SDK. One approach might be to always requestConsentInfoUpdate and call isConsentFormAvailable inside onConsentInfoUpdateSuccess. This method returns false if the user is not from EEA.

  3. I am always getting consent type consentInformation.getConsentType() 0 or UnKnown. I have tried with different combination but always 0.

  4. Is it required to forward consent information to AdMob SDK or SDK will handle it.

  5. Regarding mediation, I need the consent information but do not know how to get it. From docs: The UMP SDK writes consent status information to local storage

  6. In AdMob -> EU user consent, One of my mediation partners is not included in the Commonly used set of ad technology providers. If I use Custom set of ad technology providers, do I need to include all of Commonly used set of ad technology providers where there are 198 ad tech providers. Or including ad tech providers in Funding Choices is enough.

5

There are 5 answers

6
YAQ On

As far as I experience / understand Google Funding Choices through User Messaging Platform (actually it is not even clear why this has two different name) is TOTALLY USELESS WITHIN THE EU.

You are welcome to correct me, but as I experience on 8.3.2021:

If user clicks “Manage options” and then “Submit” and leaves the “Store and/or access information on a device” switch OFF then AdMob does not show any advertisement to the user. Thus, you may end up paying for resources (cloud services, employee, etc.) to provide a free app to your user. Based on the starting date of emerging the issue (the date of the posts and comments I see in this topic) it is a low priority problem to Google and/or to AdMob. consentInformation.getConsentType() always returns value 0. This actually proves (or at least I think) that how low priority this issue has on their list. It would be possible to check whether user consented to serve non-personalized ads at least through this getter. Then we could show him instructions how to properly opt-out and let him use the app for free. However, it seems that this is out of the interests of the developers.

Once again, anybody is welcome to correct me, maybe only I had this negative experience.

43
Tyler V On

I've been working through this myself and while I don't have answers to all your questions, I have figured out a few of them.

The UMP writes its output to some strings in SharedPreferences, outlined here. You can write some helper methods to query these strings to find out what level of ad consent the user has given or whether the user is EEA or not.

  1. How to check if the user is EEA? You can check the IABTCF_gdprApplies integer in SharedPreferences and if it is 1, the user is EEA. If it is 0 the user is not.

  2. How to get the consent type? This part gets more complicated. The Google docs here outline what permissions are needed for personalized and non-personalized ads. To get this you need to look at 4 strings from the SharedPreference: IABTCF_PurposeConsents, IABTCF_PurposeLegitimateInterests, IABTCF_VendorConsents and IABTCF_VendorLegitimateInterests. As others have noted, it is nearly impossible for a user to actually select the non-personalized ad configuration since they have to not only select "Store Information on Device" but also scroll through hundreds of non-alphabetically organized vendors to find and also select "Google" (vendor ID 755 in those strings). This means that for all practical purposes they will either select personalized ads (Consent All) or have a nice ad-free app they paid nothing for. You can at least use these checks to put up a paywall, disable Cloud features, or otherwise handle that scenario as you see fit.

I made some helper methods to find these states.

Kotlin

fun isGDPR(): Boolean {
    val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
    val gdpr = prefs.getInt("IABTCF_gdprApplies", 0)
    return gdpr == 1
}

fun canShowAds(): Boolean {
    val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)

    //https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
    //https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841

    val purposeConsent = prefs.getString("IABTCF_PurposeConsents", "") ?: ""
    val vendorConsent = prefs.getString("IABTCF_VendorConsents","") ?: ""
    val vendorLI = prefs.getString("IABTCF_VendorLegitimateInterests","") ?: ""
    val purposeLI = prefs.getString("IABTCF_PurposeLegitimateInterests","") ?: ""

    val googleId = 755
    val hasGoogleVendorConsent = hasAttribute(vendorConsent, index=googleId)
    val hasGoogleVendorLI = hasAttribute(vendorLI, index=googleId)

    // Minimum required for at least non-personalized ads
    return hasConsentFor(listOf(1), purposeConsent, hasGoogleVendorConsent)
            && hasConsentOrLegitimateInterestFor(listOf(2,7,9,10), purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)

}

fun canShowPersonalizedAds(): Boolean {
    val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)

    //https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
    //https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841

    val purposeConsent = prefs.getString("IABTCF_PurposeConsents", "") ?: ""
    val vendorConsent = prefs.getString("IABTCF_VendorConsents","") ?: ""
    val vendorLI = prefs.getString("IABTCF_VendorLegitimateInterests","") ?: ""
    val purposeLI = prefs.getString("IABTCF_PurposeLegitimateInterests","") ?: ""

    val googleId = 755
    val hasGoogleVendorConsent = hasAttribute(vendorConsent, index=googleId)
    val hasGoogleVendorLI = hasAttribute(vendorLI, index=googleId)

    return hasConsentFor(listOf(1,3,4), purposeConsent, hasGoogleVendorConsent)
            && hasConsentOrLegitimateInterestFor(listOf(2,7,9,10), purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}

// Check if a binary string has a "1" at position "index" (1-based)
private fun hasAttribute(input: String, index: Int): Boolean {
    return input.length >= index && input[index-1] == '1'
}

// Check if consent is given for a list of purposes
private fun hasConsentFor(purposes: List<Int>, purposeConsent: String, hasVendorConsent: Boolean): Boolean {
    return purposes.all { p -> hasAttribute(purposeConsent, p)} && hasVendorConsent
}

// Check if a vendor either has consent or legitimate interest for a list of purposes
private fun hasConsentOrLegitimateInterestFor(purposes: List<Int>, purposeConsent: String, purposeLI: String, hasVendorConsent: Boolean, hasVendorLI: Boolean): Boolean {
    return purposes.all { p ->
            (hasAttribute(purposeLI, p) && hasVendorLI) ||
            (hasAttribute(purposeConsent, p) && hasVendorConsent)
    }
}

Note PreferenceManager.getDefaultSharedPreferences is not deprecated - you just need to make sure to include the androidx import (import androidx.preference.PreferenceManager). If you include the wrong one (import android.preference.PreferenceManager), it will be marked as deprecated.

Swift

func isGDPR() -> Bool {
    let settings = UserDefaults.standard
    let gdpr = settings.integer(forKey: "IABTCF_gdprApplies")
    return gdpr == 1
}

// Check if a binary string has a "1" at position "index" (1-based)    
private func hasAttribute(input: String, index: Int) -> Bool {
    return input.count >= index && String(Array(input)[index-1]) == "1"
}

// Check if consent is given for a list of purposes
private func hasConsentFor(_ purposes: [Int], _ purposeConsent: String, _ hasVendorConsent: Bool) -> Bool {
    return purposes.allSatisfy { i in hasAttribute(input: purposeConsent, index: i) } && hasVendorConsent
}

// Check if a vendor either has consent or legitimate interest for a list of purposes
private func hasConsentOrLegitimateInterestFor(_ purposes: [Int], _ purposeConsent: String, _ purposeLI: String, _ hasVendorConsent: Bool, _ hasVendorLI: Bool) -> Bool {
    return purposes.allSatisfy { i in
        (hasAttribute(input: purposeLI, index: i) && hasVendorLI) ||
        (hasAttribute(input: purposeConsent, index: i) && hasVendorConsent)
    }
}

private func canShowAds() -> Bool {
    let settings = UserDefaults.standard
    
    //https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
    //https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
    
    let purposeConsent = settings.string(forKey: "IABTCF_PurposeConsents") ?? ""
    let vendorConsent = settings.string(forKey: "IABTCF_VendorConsents") ?? ""
    let vendorLI = settings.string(forKey: "IABTCF_VendorLegitimateInterests") ?? ""
    let purposeLI = settings.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
    
    let googleId = 755
    let hasGoogleVendorConsent = hasAttribute(input: vendorConsent, index: googleId)
    let hasGoogleVendorLI = hasAttribute(input: vendorLI, index: googleId)
    
    // Minimum required for at least non-personalized ads
    return hasConsentFor([1], purposeConsent, hasGoogleVendorConsent)
        && hasConsentOrLegitimateInterestFor([2,7,9,10], purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
                         
}

private func canShowPersonalizedAds() -> Bool {
    let settings = UserDefaults.standard
            
    //https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
    //https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
          
    // required for personalized ads
    let purposeConsent = settings.string(forKey: "IABTCF_PurposeConsents") ?? ""
    let vendorConsent = settings.string(forKey: "IABTCF_VendorConsents") ?? ""
    let vendorLI = settings.string(forKey: "IABTCF_VendorLegitimateInterests") ?? ""
    let purposeLI = settings.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
    
    let googleId = 755
    let hasGoogleVendorConsent = hasAttribute(input: vendorConsent, index: googleId)
    let hasGoogleVendorLI = hasAttribute(input: vendorLI, index: googleId)
    
    return hasConsentFor([1,3,4], purposeConsent, hasGoogleVendorConsent)
        && hasConsentOrLegitimateInterestFor([2,7,9,10], purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}

Edit: Example integration

Here is an example implementation of a ConsentHelper method (in Kotlin) for managing calling the UMP SDK and handling the results. This would be called on app load (e.g. in the activity onCreate) with

ConsentHelper.obtainConsentAndShow(activity) {
    // code to load ads
}

This handles waiting to initialize the MobileAds SDK until after obtaining consent, and then uses a callback to begin loading ads after the consent workflow is complete.

object ConsentHelper {
    private var isMobileAdsInitializeCalled = AtomicBoolean(false)
    private var showingForm = false
    private var showingWarning = false

    private fun initializeMobileAdsSdk(context: Context) {
        if (isMobileAdsInitializeCalled.getAndSet(true)) {
            return
        }

        // Initialize the Google Mobile Ads SDK.
        MobileAds.initialize(context)
    }

    // Called from app settings to determine whether to 
    // show a button so the user can launch the dialog
    fun isUpdateConsentButtonRequired(context: Context) : Boolean {
        val consentInformation = UserMessagingPlatform.getConsentInformation(context)
        return consentInformation.privacyOptionsRequirementStatus ==
                ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
    }

    // Called when the user clicks the button to launch
    // the CMP dialog and change their selections
    fun updateConsent(context: Activity) {
        UserMessagingPlatform.showPrivacyOptionsForm(context) { error ->
            val ci = UserMessagingPlatform.getConsentInformation(context)
            handleConsentResult(context, ci, loadAds = {})
        }
    }

    // Called from onCreate or on app load somewhere
    fun obtainConsentAndShow(context: AppCompatActivity, loadAds: ()->Unit) {

        val params = if( BuildConfig.DEBUG ) {
            val debugSettings = ConsentDebugSettings.Builder(context)
                .setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA)
                .addTestDeviceHashedId("YOUR_DEVICE_ID") // Get ID from Logcat
                .build()
            ConsentRequestParameters
                .Builder()
                .setTagForUnderAgeOfConsent(false)
                .setConsentDebugSettings(debugSettings)
                .build()
        }
        else {
            ConsentRequestParameters
                .Builder()
                .setTagForUnderAgeOfConsent(false)
                .build()
        }

        val ci = UserMessagingPlatform.getConsentInformation(context)
        ci.requestConsentInfoUpdate(
            context,
            params,
            {   // Load and show the consent form. Add guard to prevent showing form more than once at a time.
                if( showingForm ) return@requestConsentInfoUpdate

                showingForm = true
                UserMessagingPlatform.loadAndShowConsentFormIfRequired(context) { error: FormError? ->
                    showingForm = false
                    handleConsentResult(context, ci, loadAds)
                }
            },
            { error ->
                // Consent gathering failed.
                Log.w("AD_HANDLER", "${error.errorCode}: ${error.message}")
            })

        // Consent has been gathered already, load ads
        if( ci.canRequestAds() ) {
            initializeMobileAdsSdk(context.applicationContext)
            loadAds()
        }
    }

    private fun handleConsentResult(context: Activity, ci: ConsentInformation, loadAds: ()->Unit) {

        // Consent has been gathered.
        if( ci.canRequestAds() ) {
            initializeMobileAdsSdk(context.applicationContext)
            logConsentChoices(context)
            loadAds()
        }
        else {
            // This is an error state - should never get here
            logConsentChoices(context)
        }
    }

    private fun logConsentChoices(context: Activity) {
        // After completing the consent workflow, check the
        // strings in SharedPreferences to see what they
        // consented to and act accordingly
        val canShow = canShowAds(context)
        val isEEA = isGDPR(context)

        // Check what level of consent the user actually provided
        println("TEST:    user consent choices")
        println("TEST:      is EEA = $isEEA")
        println("TEST:      can show ads = $canShow")
        println("TEST:      can show personalized ads = ${canShowPersonalizedAds(context)}")

        if( !isEEA ) return

        // handle user choice, activate trial mode, etc

    }
}
2
Simon On

I would like to bring some thoughts:

1. Is it required to call MobileAds.initialize() after getting the requesting consent?

Yes it is

2. How would I check if a user is not from EEA?

You can use consentInformation.getConsentStatus() like this:

if (consentInformation.getConsentStatus()== ConsentInformation.ConsentStatus.NOT_REQUIRED) {

}

You can test this function with this:

ConsentRequestParameters.Builder consentRequest = new ConsentRequestParameters.Builder()
    .setTagForUnderAgeOfConsent(false);
        
ConsentDebugSettings debugSettings = new ConsentDebugSettings.Builder(activity)
    .setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA)
    //.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA)
    //.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_DISABLED)
    .addTestDeviceHashedId("Your device Hashed Id")
    .build();
    
consentRequest.setConsentDebugSettings(debugSettings);
ConsentRequestParameters consentRequestParameters = consentRequest.build()

But don't forget to call consentInformation.reset() each time.

3. I am always getting consent type consentInformation.getConsentType() 0.

getConsentType() is useless and was removed in user-messaging-platform:2.0.0. For me the reason is simple: with this new platform, there's no more a double state, the user granted, the user didn't granted. Now it's more like a 3 states: 1-yes_for_all, 2-no_for_all, 3-customized_by_user

4. Is it required to forward consent information to AdMob SDK or SDK will handle it.

Admob SDK will handle it. That's why you don't need the getConsentType() unless you wanted to show the user choices. But for that, it just better to reopen the consent form. Too bad the consent form doesn't load the correct settings of the user.

5. Regarding mediation, I need the consent information but do not know how to get it.

Here as stated by @Tyler V.

6. In AdMob -> EU user consent, One of my mediation partners is not included in the Commonly used set of ad technology providers. If I use Custom set of ad technology providers, do I need to include all of Commonly used set of ad technology providers where there are 198 ad tech providers. Or including ad tech providers in Funding Choices is enough.

I think including ad tech providers in Funding Choices is enough.

0
gmongi On

I have the same doubts, in particular on point 3, even in my case the consent type is always 0. I saw in this video that based on that value a different adRequest is initialized

About point 2, I set the Consentdebugsettings with user no EEA (DEBUG_GEOGRAPHY_NOT_EEA,little different from what was done on the official guide) and the method consentInformation.isConsentFormAvailable() returned false

2
Dabbel On

In case of using Flutter and interested in the current consent status, it seems possible by using:

iabtcf_consent_info

https://pub.dev/packages/iabtcf_consent_info

Using the package above with this helper class works fine for me.

Call canShowAds() to figure out whether the user should hit a paywall.

/// Call canShowAds() to determine whether ads are to be shown at all.
/// Useful for setting up a paywall.
///
/// Methods return NULL if no consent info could be read (yet).
class AdmobConsentHelper {

  /// General Data Protection Regulation (EU) (GDPR) is a regulation
  /// in EU law on data protection and privacy in the European Union (EU)
  /// and the European Economic Area (EEA).
  Future<bool?> isGDPR() async {
    return (await _consentInfo())?.gdprApplies;
  }

  Future<bool?> canShowAds() async {
    // https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details

    // https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841

    BasicConsentInfo? info = await _consentInfo();
    if (info != null) {
      if (info is ConsentInfo) {
        List<DataUsagePurpose> list1 = [
          DataUsagePurpose.storeAndAccessInformationOnADevice // 1
        ];

        List<DataUsagePurpose> list2 = [
          DataUsagePurpose.selectBasicAds, // 2
          DataUsagePurpose.measureAdPerformance, // 7
          DataUsagePurpose.applyMarketResearchToGenerateAudienceInsights, // 9
          DataUsagePurpose.developAndImproveProducts // 10
        ];

        return _hasConsent(info, list1) &&
            _hasConsentOrLegitimateInterest(info, list2);
      }
      return true;
    }
    return null;
  }

  Future<bool?> canShowPersonalizedAds() async {
    // https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details

    // https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841

    BasicConsentInfo? info = await _consentInfo();
    if (info != null) {
      if (info is ConsentInfo) {
        List<DataUsagePurpose> list1 = [
          DataUsagePurpose.storeAndAccessInformationOnADevice, // 1
          DataUsagePurpose.createAPersonalisedAdsProfile, // 3
          DataUsagePurpose.selectPersonalisedAds, // 4
        ];

        List<DataUsagePurpose> list2 = [
          DataUsagePurpose.selectBasicAds, // 2
          DataUsagePurpose.measureAdPerformance, // 7
          DataUsagePurpose.applyMarketResearchToGenerateAudienceInsights, // 9
          DataUsagePurpose.developAndImproveProducts // 10
        ];

        return _hasConsent(info, list1) &&
            _hasConsentOrLegitimateInterest(info, list2);
      }
      return true;
    }
    return null;
  }

  _hasConsentOrLegitimateInterest(
      ConsentInfo info, List<DataUsagePurpose> purposes) {
    return purposes.every((purpose) =>
        info.publisherConsent.contains(purpose) ||
        info.publisherLegitimateInterests.contains(purpose));
  }

  _hasConsent(ConsentInfo info, List<DataUsagePurpose> purposes) {
    return purposes.every((purpose) => info.publisherConsent.contains(purpose));
  }

  Future<BasicConsentInfo?> _consentInfo() async {
    return await IabtcfConsentInfo.instance.currentConsentInfo();
  }
}