@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.
@@ -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(
@@ -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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ququplay/admob",
3
- "version": "8.2.0",
3
+ "version": "8.2.1",
4
4
  "description": "A native plugin for AdMob",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",