@ququplay/admob 8.2.0 → 8.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +2 -0
- package/android/src/main/java/com/getcapacitor/community/admob/consent/AdConsentExecutor.java +89 -0
- package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +2 -0
- package/ios/Sources/AdMobPlugin/Consent/ConsentExecutor.swift +59 -0
- package/package.json +1 -1
|
@@ -72,6 +72,8 @@ public class AdMob extends Plugin {
|
|
|
72
72
|
@PluginMethod
|
|
73
73
|
public void initialize(final PluginCall call) {
|
|
74
74
|
this.setRequestConfiguration(call);
|
|
75
|
+
// The Unity Ads adapter starts Unity Ads here: give it the stored consent answer first.
|
|
76
|
+
adConsentExecutor.passConsentToUnityAds();
|
|
75
77
|
|
|
76
78
|
try {
|
|
77
79
|
MobileAds.initialize(
|
package/android/src/main/java/com/getcapacitor/community/admob/consent/AdConsentExecutor.java
CHANGED
|
@@ -17,10 +17,18 @@ import com.google.android.ump.ConsentInformation;
|
|
|
17
17
|
import com.google.android.ump.ConsentRequestParameters;
|
|
18
18
|
import com.google.android.ump.UserMessagingPlatform;
|
|
19
19
|
import java.util.ArrayList;
|
|
20
|
+
import java.util.Arrays;
|
|
20
21
|
import java.util.List;
|
|
21
22
|
|
|
22
23
|
public class AdConsentExecutor extends Executor {
|
|
23
24
|
|
|
25
|
+
// Unity Ads in Google's Additional Consent list (https://storage.googleapis.com/tcfac/additional-consent-providers.csv).
|
|
26
|
+
private static final String UNITY_ADS_PROVIDER_ID = "3234";
|
|
27
|
+
|
|
28
|
+
// Consent last updated before this date (2026-10-27) may not mention Unity Ads, which joined the ad partners in
|
|
29
|
+
// October 2026. Newer consent is never asked for again on this account.
|
|
30
|
+
private static final long UNITY_ADS_CONSENT_SINCE_MILLIS = 1793059200000L;
|
|
31
|
+
|
|
24
32
|
private ConsentInformation consentInformation;
|
|
25
33
|
|
|
26
34
|
public AdConsentExecutor(
|
|
@@ -74,6 +82,7 @@ public class AdConsentExecutor extends Executor {
|
|
|
74
82
|
.remove("IABTCF_PurposeLegitimateInterests")
|
|
75
83
|
.remove("IABTCF_VendorLegitimateInterests")
|
|
76
84
|
.remove("IABTCF_gdprApplies") // Force SDK to reassess context
|
|
85
|
+
.remove("IABTCF_AddtlConsent")
|
|
77
86
|
.apply();
|
|
78
87
|
Log.w(logTag, "Successfully purged all IABTCF tracking variables from SharedPreferences.");
|
|
79
88
|
}
|
|
@@ -134,9 +143,86 @@ public class AdConsentExecutor extends Executor {
|
|
|
134
143
|
return true;
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
// 6. Consent that does not mention Unity Ads makes Unity Ads ask in its own dialog, inside an ad.
|
|
147
|
+
// Ask once more, with Unity Ads now among the ad partners.
|
|
148
|
+
long lastUpdated = lastUpdatedMillis(tcString, base64);
|
|
149
|
+
if (gdprApplies(prefs) && lastUpdated >= 0 && lastUpdated < UNITY_ADS_CONSENT_SINCE_MILLIS && unityAdsConsent(prefs) == null) {
|
|
150
|
+
clearAllTCFPreferences(prefs);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
137
154
|
return false;
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
/** The TC string's "last updated" date (characters 7 to 12), or -1 when it is unreadable. */
|
|
158
|
+
private long lastUpdatedMillis(String tcString, String base64) {
|
|
159
|
+
if (tcString.length() < 13) {
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
long deciseconds = 0L;
|
|
163
|
+
for (char c : tcString.substring(7, 13).toCharArray()) {
|
|
164
|
+
int value = base64.indexOf(c);
|
|
165
|
+
if (value == -1) {
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
168
|
+
deciseconds = deciseconds * 64 + value;
|
|
169
|
+
}
|
|
170
|
+
return deciseconds * 100;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private boolean gdprApplies(SharedPreferences prefs) {
|
|
174
|
+
try {
|
|
175
|
+
return prefs.getInt("IABTCF_gdprApplies", 0) == 1;
|
|
176
|
+
} catch (ClassCastException ex) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unity Ads' answer in Google's Additional Consent string ("2~consented ids~dv.disclosed ids"):
|
|
183
|
+
* true or false, or null when the string does not mention Unity Ads.
|
|
184
|
+
*/
|
|
185
|
+
private Boolean unityAdsConsent(SharedPreferences prefs) {
|
|
186
|
+
String additionalConsent;
|
|
187
|
+
try {
|
|
188
|
+
additionalConsent = prefs.getString("IABTCF_AddtlConsent", "");
|
|
189
|
+
} catch (ClassCastException ex) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
String[] parts = additionalConsent == null ? new String[0] : additionalConsent.split("~");
|
|
193
|
+
if (parts.length > 1 && Arrays.asList(parts[1].split("\\.")).contains(UNITY_ADS_PROVIDER_ID)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (parts.length > 2 && Arrays.asList(parts[2].split("\\.")).contains(UNITY_ADS_PROVIDER_ID)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Tells Unity Ads the player's answer, so it never shows its own consent dialog. The AdMob adapter passes it only
|
|
204
|
+
* when the Additional Consent string mentions Unity Ads: here not mentioned counts as no. Apps without Unity Ads
|
|
205
|
+
* skip this.
|
|
206
|
+
*/
|
|
207
|
+
public void passConsentToUnityAds() {
|
|
208
|
+
Context context = contextSupplier.get();
|
|
209
|
+
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
|
|
210
|
+
if (!gdprApplies(prefs)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
boolean consent = Boolean.TRUE.equals(unityAdsConsent(prefs));
|
|
214
|
+
try {
|
|
215
|
+
Class<?> metaDataClass = Class.forName("com.unity3d.ads.metadata.MetaData");
|
|
216
|
+
Object metaData = metaDataClass.getConstructor(Context.class).newInstance(context.getApplicationContext());
|
|
217
|
+
metaDataClass.getMethod("set", String.class, Object.class).invoke(metaData, "gdpr.consent", consent);
|
|
218
|
+
metaDataClass.getMethod("commit").invoke(metaData);
|
|
219
|
+
} catch (ClassNotFoundException ex) {
|
|
220
|
+
// No Unity Ads in this app.
|
|
221
|
+
} catch (ReflectiveOperationException ex) {
|
|
222
|
+
Log.w(logTag, "Could not pass consent to Unity Ads", ex);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
140
226
|
public boolean canShowAds() {
|
|
141
227
|
Context context = contextSupplier.get();
|
|
142
228
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
|
|
@@ -204,6 +290,7 @@ public class AdConsentExecutor extends Executor {
|
|
|
204
290
|
// Check (and possibly purge) outdated consent before reading the
|
|
205
291
|
// remaining values, so they reflect the post-cleanup state.
|
|
206
292
|
boolean isConsentOutdated = isConsentOutdated();
|
|
293
|
+
passConsentToUnityAds();
|
|
207
294
|
|
|
208
295
|
JSObject consentInfo = new JSObject();
|
|
209
296
|
consentInfo.put("status", getConsentStatusString(consentInformation.getConsentStatus()));
|
|
@@ -236,6 +323,7 @@ public class AdConsentExecutor extends Executor {
|
|
|
236
323
|
if (formError != null) {
|
|
237
324
|
call.reject("Error when show privacy form", formError.getMessage());
|
|
238
325
|
} else {
|
|
326
|
+
passConsentToUnityAds();
|
|
239
327
|
call.resolve();
|
|
240
328
|
}
|
|
241
329
|
})
|
|
@@ -268,6 +356,7 @@ public class AdConsentExecutor extends Executor {
|
|
|
268
356
|
// Check (and possibly purge) outdated consent before reading the
|
|
269
357
|
// remaining values, so they reflect the post-cleanup state.
|
|
270
358
|
boolean isConsentOutdated = isConsentOutdated();
|
|
359
|
+
passConsentToUnityAds();
|
|
271
360
|
|
|
272
361
|
JSObject consentFormInfo = new JSObject();
|
|
273
362
|
consentFormInfo.put("status", getConsentStatusString(consentInformation.getConsentStatus()));
|
|
@@ -56,6 +56,8 @@ public class AdMobPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
56
56
|
self.adInterstitialExecutor.plugin = self
|
|
57
57
|
self.consentExecutor.plugin = self
|
|
58
58
|
self.setRequestConfiguration(call)
|
|
59
|
+
// The Unity Ads adapter starts Unity Ads here: give it the stored consent answer first.
|
|
60
|
+
self.consentExecutor.passConsentToUnityAds()
|
|
59
61
|
|
|
60
62
|
MobileAds.shared.start(completionHandler: nil)
|
|
61
63
|
call.resolve([:])
|
|
@@ -6,6 +6,13 @@ import UserMessagingPlatform
|
|
|
6
6
|
class ConsentExecutor: NSObject {
|
|
7
7
|
weak var plugin: AdMobPlugin?
|
|
8
8
|
|
|
9
|
+
// Unity Ads in Google's Additional Consent list (https://storage.googleapis.com/tcfac/additional-consent-providers.csv).
|
|
10
|
+
private let unityAdsProviderId = "3234"
|
|
11
|
+
|
|
12
|
+
// Consent last updated before this date (2026-10-27) may not mention Unity Ads, which joined the ad partners in
|
|
13
|
+
// October 2026. Newer consent is never asked for again on this account.
|
|
14
|
+
private let unityAdsConsentSinceMillis: Int64 = 1793059200000
|
|
15
|
+
|
|
9
16
|
func requestConsentInfo(_ call: CAPPluginCall, _ debugGeography: Int, _ testDeviceIdentifiers: [String], _ tagForUnderAgeOfConsent: Bool) {
|
|
10
17
|
let parameters = RequestParameters()
|
|
11
18
|
let debugSettings = DebugSettings()
|
|
@@ -37,6 +44,7 @@ class ConsentExecutor: NSObject {
|
|
|
37
44
|
Task {
|
|
38
45
|
do {
|
|
39
46
|
try await ConsentForm.presentPrivacyOptionsForm(from: rootViewController)
|
|
47
|
+
passConsentToUnityAds()
|
|
40
48
|
call.resolve()
|
|
41
49
|
} catch {
|
|
42
50
|
call.reject("Failed to show privacy options form: \(error.localizedDescription)")
|
|
@@ -74,6 +82,7 @@ class ConsentExecutor: NSObject {
|
|
|
74
82
|
// Check (and possibly purge) outdated consent before reading the
|
|
75
83
|
// remaining values, so they reflect the post-cleanup state.
|
|
76
84
|
let isConsentOutdated = isConsentOutdated()
|
|
85
|
+
passConsentToUnityAds()
|
|
77
86
|
|
|
78
87
|
return [
|
|
79
88
|
"status": getConsentStatusString(ConsentInformation.shared.consentStatus),
|
|
@@ -173,6 +182,7 @@ class ConsentExecutor: NSObject {
|
|
|
173
182
|
settings.removeObject(forKey: "IABTCF_PurposeLegitimateInterests")
|
|
174
183
|
settings.removeObject(forKey: "IABTCF_VendorLegitimateInterests")
|
|
175
184
|
settings.removeObject(forKey: "IABTCF_gdprApplies") // Force SDK to reassess context
|
|
185
|
+
settings.removeObject(forKey: "IABTCF_AddtlConsent")
|
|
176
186
|
NSLog("AdMob: Successfully purged all IABTCF tracking variables from UserDefaults.")
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -227,8 +237,57 @@ class ConsentExecutor: NSObject {
|
|
|
227
237
|
return true
|
|
228
238
|
}
|
|
229
239
|
|
|
240
|
+
// 6. Consent that does not mention Unity Ads makes Unity Ads ask in its own dialog, inside an ad.
|
|
241
|
+
// Ask once more, with Unity Ads now among the ad partners.
|
|
242
|
+
if let lastUpdated = lastUpdatedMillis(tcString, base64),
|
|
243
|
+
isGDPR() && lastUpdated < unityAdsConsentSinceMillis && unityAdsConsent(settings) == nil {
|
|
244
|
+
clearAllTCFPreferences(settings)
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
|
|
230
248
|
return false
|
|
231
249
|
}
|
|
250
|
+
|
|
251
|
+
/// The TC string's "last updated" date (characters 7 to 12), or nil when it is unreadable.
|
|
252
|
+
private func lastUpdatedMillis(_ tcString: String, _ base64: String) -> Int64? {
|
|
253
|
+
guard tcString.count >= 13 else {
|
|
254
|
+
return nil
|
|
255
|
+
}
|
|
256
|
+
var deciseconds: Int64 = 0
|
|
257
|
+
for char in tcString.dropFirst(7).prefix(6) {
|
|
258
|
+
guard let value = base64.firstIndex(of: char) else {
|
|
259
|
+
return nil
|
|
260
|
+
}
|
|
261
|
+
deciseconds = deciseconds * 64 + Int64(value.utf16Offset(in: base64))
|
|
262
|
+
}
|
|
263
|
+
return deciseconds * 100
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Unity Ads' answer in Google's Additional Consent string ("2~consented ids~dv.disclosed ids"):
|
|
267
|
+
/// true or false, or nil when the string does not mention Unity Ads.
|
|
268
|
+
private func unityAdsConsent(_ settings: UserDefaults) -> Bool? {
|
|
269
|
+
let parts = (settings.string(forKey: "IABTCF_AddtlConsent") ?? "").components(separatedBy: "~")
|
|
270
|
+
if parts.count > 1 && parts[1].components(separatedBy: ".").contains(unityAdsProviderId) {
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
if parts.count > 2 && parts[2].components(separatedBy: ".").contains(unityAdsProviderId) {
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
return nil
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Tells Unity Ads the player's answer, so it never shows its own consent dialog. The AdMob adapter passes it only
|
|
280
|
+
/// when the Additional Consent string mentions Unity Ads: here not mentioned counts as no. Apps without Unity Ads
|
|
281
|
+
/// skip this.
|
|
282
|
+
func passConsentToUnityAds() {
|
|
283
|
+
guard isGDPR(), let metaDataClass = NSClassFromString("UADSMetaData") as? NSObject.Type else {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
let consent = unityAdsConsent(UserDefaults.standard) == true
|
|
287
|
+
let metaData = metaDataClass.init()
|
|
288
|
+
_ = metaData.perform(NSSelectorFromString("set:value:"), with: "gdpr.consent", with: NSNumber(value: consent))
|
|
289
|
+
_ = metaData.perform(NSSelectorFromString("commit"))
|
|
290
|
+
}
|
|
232
291
|
func getPrivacyOptionsRequirementStatus(_ requirementStatus: PrivacyOptionsRequirementStatus) -> String {
|
|
233
292
|
switch requirementStatus {
|
|
234
293
|
case PrivacyOptionsRequirementStatus.required:
|