跳转至正文

如何在 Capacitor 中处理 AdMob GDPR 同意

在 Capacitor 应用中使用 Google UMP SDK 处理 AdMob GDPR 同意:包含同意表单、隐私选项、ATT 顺序、地理位置调试及错误处理,并配合 code。

如何在 Capacitor 中处理 AdMob GDPR 同意

要在 Capacitor 应用中处理 AdMob GDPR 同意,请使用 Google 的 User Messaging Platform (UMP) SDK:在每次启动时请求同意信息,根据需要显示同意表单,并且仅在 canRequestAds 为 true 时才初始化广告。你还需要提供隐私选项入口以便用户更改选择;在 iOS 上,UMP 流程应在 App Tracking Transparency 提示之前执行。本指南通过两种方案展示完整流程:一个封装了 UMP 的 JavaScript 插件,以及 @capgo/capacitor-admob 配合一个轻量级的 native 同意桥接器。

UMP 的作用以及为什么 AdMob 需要它

自 2024 年 1 月起,Google 要求向 EEA、英国和瑞士用户投放广告的发布商,必须使用与 IAB 透明度与同意框架 (TCF) 集成的 Google 认证同意管理平台 (CMP)。UMP SDK 是 Google 自有的认证 CMP,完全免费并内置在 AdMob 控制台中。

UMP 为您提供以下三项功能:

  1. 检测用户是否处于需要征得同意的地区。
  2. 以用户的语言显示您在 AdMob 中配置的同意信息。
  3. 将用户的选择以 TC 字符串的形式存储在设备上(标准 IABTCF_ 键值存储于 UserDefaults iOS 上以及 SharedPreferences Android 上),Google Mobile Ads SDK 和中介合作伙伴在请求广告时会读取这些信息。

您也可以通过同一个控制台,使用 UMP 来应对美国各州的隐私法规(AdMob 称之为“US states”消息)。

第 1 步:在 AdMob 中创建消息

在 AdMob 控制台中,前往 隐私与消息:

  • 创建 欧洲法规 消息 (GDPR)。选择适用的应用、语言以及广告合作伙伴列表。
  • 也可以选择创建 美国州级 消息。
  • 在 iOS 上,如果你计划请求 App Tracking Transparency,请创建一个 IDFA 说明 消息。UMP 会在系统 ATT prompt 出现前展示该消息。
  • 发布消息。未发布的消息不会显示。

如果后续应用中始终未显示表单,请首先检查消息是否未发布或未分配。

步骤 2:确定操作顺序

适用于大多数应用的顺序:

  1. 应用启动。
  2. 使用 UMP 请求同意信息更新(每次启动时)。
  3. 如果需要,加载并显示同意表单。
  4. 如果 canRequestAds 为 true,则启动 Mobile Ads SDK 并加载广告。
  5. 当 UMP 表示需要隐私选项时,在设置界面中显示“隐私设置”入口。

Google 还允许在发起同意请求的同时,并行启动 Mobile Ads SDK canRequestAds 在之前的会话中已为 true。这缩短了回访用户的首次广告加载时间。

方案 A:一个在 JavaScript 中封装 UMP 的插件

@capacitor-community/admob (version 8 支持 Capacitor 8)直接暴露了 UMP 方法: requestConsentInfo, showConsentForm, showPrivacyOptionsForm 和 resetConsentInfo.

bun add @capacitor-community/admob
bunx cap sync
import {
  AdMob,
  AdmobConsentStatus,
  AdmobConsentDebugGeography,
} from '@capacitor-community/admob';

export async function initAdsWithConsent(isDebug: boolean) {
  await AdMob.initialize();

  let info = await AdMob.requestConsentInfo(
    isDebug
      ? {
          debugGeography: AdmobConsentDebugGeography.EEA,
          testDeviceIdentifiers: ['YOUR_TEST_DEVICE_ID'],
        }
      : undefined,
  );

  if (info.isConsentFormAvailable && info.status === AdmobConsentStatus.REQUIRED) {
    info = await AdMob.showConsentForm();
  }

  // The enum value is the string 'REQUIRED'
  const showPrivacyEntry = String(info.privacyOptionsRequirementStatus) === 'REQUIRED';

  return { canRequestAds: info.canRequestAds, showPrivacyEntry };
}

export async function openPrivacyOptions() {
  await AdMob.showPrivacyOptionsForm();
}

该插件是上述顺序中的例外:其 README 要求 AdMob.initialize() 在 requestConsentInfo(),且在 iOS 上,在以下操作完成前无法显示同意表单: initialize() 已运行。 initialize() 启动 SDK 但不请求广告,因此关键规则是:在以下情况发生前,绝不要加载广告: canRequestAds 为 true。如果你需要 SDK 保持停止状态直到用户做出回答,请使用方案 B,即在以下操作之前运行同意流程: AdMob.start().

检查 canRequestAds 在每次加载广告前运行,而不仅仅是在启动时,因为用户可以从隐私选项表单中撤回同意。

方案 B:使用原生 UMP bridge 的 @capgo/capacitor-admob

@capgo/capacitor-admob 为你提供 AdMob.start(), request configuration, banner, interstitial, rewarded and rewarded interstitial ads, plus iOS tracking authorization. It does not wrap UMP, so you add a small app-local plugin that runs the consent flow before you call AdMob.start().

bun add @capgo/capacitor-admob
bunx cap sync

添加 UMP SDK

  • iOS: 添加 Google 的 GoogleUserMessagingPlatform package,使用 Swift Package Manager (Capacitor 8 项目默认使用 SPM),或者如果你的项目仍在使用 CocoaPods,则使用同名的 CocoaPods pod。
  • Android: 在 android/app/build.gradle:
dependencies {
    implementation "com.google.android.ump:user-messaging-platform:<latest version>"
}

使用 Google Maven 仓库中的最新版本。

创建 ios/App/App/ConsentPlugin.swift:

import Capacitor
import UserMessagingPlatform

@objc(ConsentPlugin)
public class ConsentPlugin: CAPPlugin, CAPBridgedPlugin {
    public let identifier = "ConsentPlugin"
    public let jsName = "Consent"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "gather", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "showPrivacyOptions", returnType: CAPPluginReturnPromise),
    ]

    @objc func gather(_ call: CAPPluginCall) {
        let parameters = RequestParameters()
        DispatchQueue.main.async {
            ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { error in
                if let error = error {
                    call.reject(error.localizedDescription)
                    return
                }
                guard let vc = self.bridge?.viewController else {
                    call.reject("No view controller")
                    return
                }
                ConsentForm.loadAndPresentIfRequired(from: vc) { formError in
                    if let formError = formError {
                        call.reject(formError.localizedDescription)
                        return
                    }
                    call.resolve(self.state())
                }
            }
        }
    }

    @objc func showPrivacyOptions(_ call: CAPPluginCall) {
        DispatchQueue.main.async {
            guard let vc = self.bridge?.viewController else {
                call.reject("No view controller")
                return
            }
            ConsentForm.presentPrivacyOptionsForm(from: vc) { formError in
                if let formError = formError {
                    call.reject(formError.localizedDescription)
                    return
                }
                call.resolve(self.state())
            }
        }
    }

    private func state() -> [String: Any] {
        return [
            "canRequestAds": ConsentInformation.shared.canRequestAds,
            "privacyOptionsRequired":
                ConsentInformation.shared.privacyOptionsRequirementStatus == .required,
        ]
    }
}

通过 CAPBridgeViewController 子类进行注册,并在 view controller 中设置该类 Main.storyboard:

import Capacitor

class MainViewController: CAPBridgeViewController {
    override open func capacitorDidLoad() {
        bridge?.registerPluginInstance(ConsentPlugin())
    }
}

创建 android/app/src/main/java/com/example/app/ConsentPlugin.java (使用您的包名):

package com.example.app;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.google.android.ump.ConsentInformation;
import com.google.android.ump.ConsentRequestParameters;
import com.google.android.ump.UserMessagingPlatform;

@CapacitorPlugin(name = "Consent")
public class ConsentPlugin extends Plugin {

    @PluginMethod
    public void gather(PluginCall call) {
        ConsentInformation info = UserMessagingPlatform.getConsentInformation(getContext());
        ConsentRequestParameters params = new ConsentRequestParameters.Builder().build();

        getActivity().runOnUiThread(() ->
            info.requestConsentInfoUpdate(
                getActivity(),
                params,
                () -> UserMessagingPlatform.loadAndShowConsentFormIfRequired(getActivity(), formError -> {
                    if (formError != null) {
                        call.reject(formError.getMessage());
                        return;
                    }
                    call.resolve(state(info));
                }),
                requestError -> call.reject(requestError.getMessage())
            )
        );
    }

    @PluginMethod
    public void showPrivacyOptions(PluginCall call) {
        getActivity().runOnUiThread(() ->
            UserMessagingPlatform.showPrivacyOptionsForm(getActivity(), formError -> {
                if (formError != null) {
                    call.reject(formError.getMessage());
                    return;
                }
                call.resolve(state(UserMessagingPlatform.getConsentInformation(getContext())));
            })
        );
    }

    private JSObject state(ConsentInformation info) {
        JSObject ret = new JSObject();
        ret.put("canRequestAds", info.canRequestAds());
        ret.put(
            "privacyOptionsRequired",
            info.getPrivacyOptionsRequirementStatus() ==
                ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
        );
        return ret;
    }
}

在其中注册 MainActivity.java 在 super.onCreate:

public class MainActivity extends BridgeActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        registerPlugin(ConsentPlugin.class);
        super.onCreate(savedInstanceState);
    }
}

通过 TypeScript 调用

import { registerPlugin } from '@capacitor/core';
import { AdMob, BannerAd } from '@capgo/capacitor-admob';

interface ConsentState {
  canRequestAds: boolean;
  privacyOptionsRequired: boolean;
}

interface ConsentPlugin {
  gather(): Promise<ConsentState>;
  showPrivacyOptions(): Promise<ConsentState>;
}

const Consent = registerPlugin<ConsentPlugin>('Consent');

export async function startAds() {
  let state: ConsentState;
  try {
    state = await Consent.gather();
  } catch (err) {
    console.warn('Consent flow failed', err);
    return { showPrivacyEntry: false };
  }

  if (state.canRequestAds) {
    await AdMob.start();
    const banner = new BannerAd({
      adUnitId: 'ca-app-pub-3940256099942544/2934735716', // Google test banner (iOS)
      position: 'bottom',
    });
    await banner.show();
  }

  return { showPrivacyEntry: state.privacyOptionsRequired };
}

export const openPrivacyOptions = () => Consent.showPrivacyOptions();

Native code 变更需要重新构建 Store 版本。之后,设置页面的同意文案和广告投放逻辑即可通过 Capgo live updates 发布.

步骤 3:添加隐私选项入口

当 UMP 报告需要隐私选项时,Google 要求必须提供可见的方式来重新打开同意选项。在您的设置页面添加一个“隐私设置”行,并在不需要时将其隐藏:

const { showPrivacyEntry } = await startAds();
document.querySelector('#privacy-row')?.toggleAttribute('hidden', !showPrivacyEntry);
document.querySelector('#privacy-row')?.addEventListener('click', () => openPrivacyOptions());

用户更改选择后,请读取 canRequestAds 并在下一次广告请求前再次读取。

第 4 步:iOS 上的 App Tracking Transparency

ATT 与 GDPR 是两个不同的概念。ATT 用于控制 iOS 上 IDFA 的访问权限,而 GDPR 同意则控制是否以及如何处理个人数据。对于 iOS 上的欧盟用户,通常两者都需要。

  • 添加 NSUserTrackingUsageDescription 至 Info.plist.
  • 如果你在 AdMob 中配置了相关消息,可以让 UMP 显示 IDFA 说明消息并触发 ATT。否则,请在同意流程结束后自行请求 ATT。

使用 @capgo/capacitor-admob 你可以直接读取并请求 ATT 状态:

import { AdMob, TrackingAuthorizationStatus } from '@capgo/capacitor-admob';

const { status } = await AdMob.trackingAuthorizationStatus();
if (status === TrackingAuthorizationStatus.notDetermined) {
  await AdMob.requestTrackingAuthorization();
}

Capgo 也有一个独立的 App Tracking Transparency 插件 如果你需要为其他 SDK 获取 ATT 权限。别忘了在隐私清单 (privacy manifest) 和 App Store 隐私标签中声明追踪行为,请参阅我们的 privacy manifest 指南.

第 5 步:使用调试地理位置进行测试

UMP 仅向位于 EEA、英国或瑞士的用户显示 GDPR 表单。若要在其他地区进行测试:

  • 运行一次 App,并在日志中找到包含设备哈希 ID 的 UMP 日志行。
  • 将其添加为测试设备,并将 debug geography 设置为 EEA。使用社区插件时,传入 debugGeography 和 testDeviceIdentifiers 至 requestConsentInfo。在原生桥接层中,设置 DebugSettings (iOS 端)以及 ConsentDebugSettings (Android 端),且仅限 debug builds。
  • 在多次运行之间重置授权状态(resetConsentInfo() 使用社区插件时, ConsentInformation.reset() 原生方式)以便再次显示表单。

切勿在 release builds 中包含 debug 设置。请务必通过 build flag 进行控制。

第 6 步:处理错误且不阻塞应用

同意请求可能会失败:例如网络断开、App ID 配置错误或未发布消息。请将失败视为“暂时不显示广告”,而不是应用崩溃:

  • 如果 requestConsentInfoUpdate 失败但 canRequestAds 在之前的会话中为 true,你仍然可以请求广告。
  • 如果表单加载失败,请记录日志并在下次启动时重试。
  • 在同意表单显示时,切勿展示广告。

故障排除

表单从未显示。 消息未发布、未分配给此 App ID,或者你处于 EEA 以外且未设置调试地理位置。请检查以下内容中的 AdMob App ID: Info.plist 和 AndroidManifest.xml 与控制台一致。

表单会在每次启动时显示。 用户未做选择直接关闭了表单,或者你在生产环境 code 中调用了 reset。

canRequestAds 针对美国用户返回 false。 你发布了针对美国各州的消息,且用户选择了拒绝出售或共享信息。请仅通过 SDK 提供受限广告,切勿绕过它。

聚合合作伙伴未获得授权。 请确保每个合作伙伴的 adapter 都是最新版本,并已列入 GDPR 消息的广告合作伙伴列表中。

iOS ATT prompt 从未出现。 NSUserTrackingUsageDescription 缺失,或者用户在设置中禁用了“允许 App 请求追踪”。

表单之外的合规性

UMP 处理广告端。你的 App 仍需提供隐私政策,其中应列出 AdMob、数据保留规则以及处理数据请求的方式。我们的 GDPR 合规检查清单 涵盖了 App 的其余部分。

为 Capacitor 应用提供实时更新

当 Web 层 Bug 已上线时,通过 Capgo 推送修复,不必等 App Store 审核数天。用户会在后台获取更新,而原生变更仍走常规审核流程。

博客最新文章

Capgo 为你提供打造真正专业移动应用所需的深度洞察。