이전에 iOS앱에 애드몹 GDPR 메시지 대응하기 글을 올렸었는데 (이전글)
그 사이 애드몹의 UserMessagingPlatform 업데이트가 있어서 다시 수정한 버전으로 올려봅니다.
GDPR이 뭔지,
애드몹의 GDPR 메시지는 어떻게 작성하는지는 이전글을 참고해주세요.
여기서는 구현 예시만 다시 정리해서 올려봅니다.
GDPR은 유럽의 개인 정보보호 방침에 따라 사용자 기기에 어떤 데이터를 추가로 저장하고 (쿠키 같은) 그리고 사용자의 데이터가 광고 네트워크사 또는 서버에 어떤 사용자 데이터가 전달 될 지 등등을 사용자의 동의를 받는 과정입니다.
처음에는 UMPConsentForm.load { } 으로 form 을 로드한 뒤에 form.present { } 로 form을 노출해서 사용자에게 동의를 요청합니다.
만약 사용자가 이를 동의하지 않는 경우에는 (링크) 를 보면 목적 1에 따라 사용자의 기기에 데이터를 저장하지 않는다고 하면 광고 노출도 불가 합니다.
이에 따라 클라이언트 개발자는 사용자가 광고 노출에 동의하지 않는 경우에 어떻게 할지 결정이 필요합니다.
아래에서 canShowGDPRForm() 를 먼저 호출해서 GDPR 동의를 받아야 하는지를 체크합니다.
GDPRForm을 보여줘야 할 경우 사용자에게 showGDPRForm() 를 호출해서 GDPR 동의를 요청합니다.
static func updateGDPRInfo(completionHandler: @escaping () -> Void) {
guard UMPConsentInformation.sharedInstance.consentStatus == .unknown else {
completionHandler()
return
}
UMPConsentInformation.sharedInstance
.requestConsentInfoUpdate(with: gdprParameters,
completionHandler: { _ in
completionHandler()
})
}
static func canShowGDPRForm() async -> Bool {
await withCheckedContinuation { continuation in
updateGDPRInfo {
let formStatus = UMPConsentInformation.sharedInstance.formStatus
continuation.resume(returning: formStatus == UMPFormStatus.available)
}
}
}
static func showGDPRForm(completionHandler: @escaping (Bool) -> Void) async {
guard await canShowGDPRForm() else {
completionHandler(false)
return
}
let formStatus = UMPConsentInformation.sharedInstance.formStatus
guard formStatus == UMPFormStatus.available else {
completionHandler(false)
return
}
guard UMPConsentInformation.sharedInstance.consentStatus == UMPConsentStatus.required else {
completionHandler(false)
return
}
DispatchQueue.main.async {
guard let vc = UIApplication.topViewController() else {
completionHandler(false)
return
}
UMPConsentForm.load { form, _ in
guard let form = form else {
completionHandler(false)
return
}
form.present(
from: vc,
completionHandler: { _ in
Log.d("UMPConsentInformation.sharedInstance.consentStatus after : \(UMPConsentInformation.sharedInstance.consentStatus)")
if UMPConsentInformation.sharedInstance.consentStatus == UMPConsentStatus.obtained {
// App can start requesting ads.
completionHandler(true)
return
}
// Handle dismissal by reloading form.
Task.detached {
await self.showGDPRForm(completionHandler: completionHandler)
}
})
}
}
}
GDPR 동의 요청 이후 또는 GDPRForm을 보여줄 수 없는 상태인 경우 (동의를 받았거나, 또는 거절당했거나, 동의가 필요없는 경우 (유럽 외 지역)) 에는 canShowAds() 를 호출합니다.
(아무튼 무조건 호출합니다.)
아래 코드는 https://stackoverflow.com/questions/65351543/how-to-implement-ump-sdk-correctly-for-eu-consent/68310602#68310602 에서 가져왔습니다.
아래 코드를 이용해서 canShowAds() 를 체크해서 광고를 보여줄 수 있는 상태인지를 체크합니다.
GDPR 사용지역이 아닌 경우, 또는 이미 동의를 받은 경우에 true 가 return 됩니다.
// Check if a binary string has a "1" at position "index" (1-based)
private static 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 static 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 static 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)
}
}
static func canShowAds() -> Bool {
if UMPConsentInformation.sharedInstance.consentStatus == .notRequired {
return true
}
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)
}
static func canShowPersonalizedAds() -> Bool {
if CoreConstants.isiOSAppOnMac
|| ATTrackingManager.trackingAuthorizationStatus == .denied
|| ATTrackingManager.trackingAuthorizationStatus == .restricted {
return false
}
if UMPConsentInformation.sharedInstance.consentStatus == .notRequired {
return true
}
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)
}
만약 canShowAds() 가 true 이면 계속 진행하고,
canShowAds() 가 false 인 경우에는 사용자에게 광고를 반드시 보여줘야 하는 경우에는 재동의를 받기 위한 화면을 띄웁니다. 사용자에게 기능을 제한 할 경우에는 적절히 기능 제한을 하면 됩니다.
재동의는 아래 코드를 이용해 받습니다.
static func reRequestGDPR(completionHandler: @escaping (Bool) -> Void) {
let formStatus = UMPConsentInformation.sharedInstance.formStatus
guard formStatus == UMPFormStatus.available else {
completionHandler(false)
return
}
DispatchQueue.main.async {
guard let vc = UIApplication.topViewController() else {
completionHandler(false)
return
}
UMPConsentForm.presentPrivacyOptionsForm(from: vc) { error in
if error != nil {
completionHandler(false)
return
}
if UMPConsentInformation.sharedInstance.consentStatus == UMPConsentStatus.obtained {
// App can start requesting ads.
completionHandler(true)
return
}
// Handle dismissal by reloading form.
DispatchQueue.main.async { [self] in
self.reRequestGDPR(completionHandler: completionHandler)
}
}
}
}
(제가 작성했지만 함수 이름은 맘에 안드네요. ㅠ)
아무튼 여기에 UMPConsentForm.presentPrivacyOptionsForm() 을 호출하면 사용자에게 재동의를 받을 수 있습니다. 이 함수는 Google UMP SDK (UserMessagingPlatform) 2.1.0 버전 2023년 7월 24일 출시 (글 쓰는 2024년 1월 5일 최신 버전)에 추가되었습니다.
(이전 글이 2023년 7월 10일에 작성했는데 ㅠ 이때는 없던 함수네요. 그래서 이전 글에는 reset()을 호출하라고 했었는데 reset()은 개발모드에서만 사용하고 프로덕션 모드에서는 사용하지 말라고 되어있습니다.)
암튼 이렇게 하면
사용자에게 동의를 구하고 -> 동의 상태 확인 및 광고 노출 가능 여부 확인 -> 광고 노출 불가 상태에서 다시 재동의를 요청. 까지의 일련의 과정을 이용할 수 있습니다.
// 추가로 이전에 애드몹 메시지에 동의 거부 버튼을 노출 안시키면 되지 않냐는 질문이 있었는데, 동의 버튼을 노출하지 않더라도 gdpr 동의 화면에서 옵션을 누르고 아무것도 선택하지 않고 confirm 할 경우 동의 거절과 동일한 동작을 하기 때문에 원천적으로 사용자의 동의를 거부하지 못하게는 할 수 없습니다.
// 끝.