Add iOS support for TapADN package

This commit is contained in:
2026-06-12 16:05:13 +08:00
parent 3341169f9b
commit 7e012bfd45
5 changed files with 621 additions and 72 deletions

View File

@@ -5,5 +5,7 @@
* 接入 Dirichlet/TapADN 聚合 Unity SDK `4.2.5.0`
* 新增 `TapadnAdController`、激励视频、插屏、开屏播放器,实现 `CC-Framework.Commercialization` 抽象层。
* 新增 Android 构建后处理Manifest 权限、TapADN FileProvider、微信 OpenSDK WXEntryActivity/queries、AndroidX/Jetifier 属性。
* 新增 iOS 构建后处理CocoaPods、SKAdNetwork、ATT 文案、AppTrackingTransparency 弱链接、GDT 动态 framework 嵌入。
* iOS 激励、插屏、开屏 auto API 增加 load-then-show 兼容 fallback并补齐展示失败回调。
* 新增 `TapadnCommercialization` 便捷入口,由实现模块创建 controller 并初始化 `ADManager`
* 官方 Demo Sample 从主包剔除,调试内容改为可选 `Samples~`

View File

@@ -25,12 +25,17 @@ namespace Dirichlet.Mediation.Editor
/// </summary>
public class DirichletMediationIOSPostProcessor
{
private const string SDKVersion = "4.2.0.2";
private const string DefaultIOSSDKVersion = "4.2.0.1";
private const string MinIOSVersion = "11.0";
private const string DefaultTrackingUsageDescription = "该标识符将用于向您投放个性化广告";
// 环境变量 override仅在解析失败或接入方工程极端定制时使用
private const string ENV_FRAMEWORK_TARGET = "DIRICHLET_UNITY_FRAMEWORK_TARGET";
private const string ENV_APP_TARGET = "DIRICHLET_UNITY_APP_TARGET";
private const string ENV_IOS_SDK_VERSION = "DIRICHLET_IOS_SDK_VERSION";
private const string ENV_ATT_DESCRIPTION = "DIRICHLET_IOS_ATT_DESCRIPTION";
private const string PrefKeyIOSSDKVersion = "Dirichlet.iOS.SDKVersion";
private const string PrefKeyATTDescription = "Dirichlet.iOS.TrackingUsageDescription";
/// <summary>
/// 解析出的 target 信息
@@ -363,6 +368,7 @@ namespace Dirichlet.Mediation.Editor
// Note: DirichletAdSDK (DRA adapter) is always enabled as core SDK
var enableCsj = EditorPrefs.GetBool("Dirichlet.iOS.EnableCSJ", true);
var enableGdt = EditorPrefs.GetBool("Dirichlet.iOS.EnableGDT", true);
var sdkVersion = ResolveIOSSDKVersion();
var podfileContent = new StringBuilder();
podfileContent.AppendLine("# Generated by Dirichlet Mediation Unity Plugin");
@@ -379,20 +385,20 @@ namespace Dirichlet.Mediation.Editor
// 所有 pods 统一放到 Framework target
// SDK 通过 NSClassFromString 查找 Adapter 类,必须在同一 target 中
podfileContent.AppendLine($"target '{targetInfo.FrameworkTargetName}' do");
podfileContent.AppendLine($" pod 'DirichletMediationSDK', '{SDKVersion}'");
podfileContent.AppendLine($" pod 'DirichletMediationSDK', '{sdkVersion}'");
if (enableCsj)
{
podfileContent.AppendLine($" pod 'DirichletMediationAdapterCSJ', '{SDKVersion}'");
podfileContent.AppendLine($" pod 'DirichletMediationAdapterCSJ', '{sdkVersion}'");
}
if (enableGdt)
{
podfileContent.AppendLine($" pod 'DirichletMediationAdapterGDT', '{SDKVersion}'");
podfileContent.AppendLine($" pod 'DirichletMediationAdapterGDT', '{sdkVersion}'");
}
// DirichletAdSDK (DRA adapter) is always included as core SDK
podfileContent.AppendLine($" pod 'DirichletMediationAdapterDRA', '{SDKVersion}'");
podfileContent.AppendLine($" pod 'DirichletMediationAdapterDRA', '{sdkVersion}'");
podfileContent.AppendLine("end");
podfileContent.AppendLine();
@@ -411,7 +417,24 @@ namespace Dirichlet.Mediation.Editor
File.WriteAllText(podfilePath, podfileContent.ToString());
Debug.Log($"[DirichletMediation] Generated Podfile at {podfilePath}");
Debug.Log($"[DirichletMediation] All pods allocated to {targetInfo.FrameworkTargetName} (CSJ={enableCsj}, GDT={enableGdt}, DRA=always)");
Debug.Log($"[DirichletMediation] All pods allocated to {targetInfo.FrameworkTargetName} (version={sdkVersion}, CSJ={enableCsj}, GDT={enableGdt}, DRA=always)");
}
private static string ResolveIOSSDKVersion()
{
var envVersion = System.Environment.GetEnvironmentVariable(ENV_IOS_SDK_VERSION);
if (!string.IsNullOrWhiteSpace(envVersion))
{
return envVersion.Trim();
}
var prefVersion = EditorPrefs.GetString(PrefKeyIOSSDKVersion, string.Empty);
if (!string.IsNullOrWhiteSpace(prefVersion))
{
return prefVersion.Trim();
}
return DefaultIOSSDKVersion;
}
/// <summary>
@@ -466,13 +489,15 @@ namespace Dirichlet.Mediation.Editor
Debug.Log($" Framework target: {targetInfo.FrameworkTargetName} (GUID: {targetGuid})");
Debug.Log($" App target: {targetInfo.AppTargetName} (GUID: {mainTargetGuid})");
// NOTE: System frameworks (AdSupport, AVFoundation, WebKit, CoreVideo, etc.)
// NOTE: Most system frameworks (AdSupport, AVFoundation, WebKit, CoreVideo, etc.)
// are declared in SDK podspecs and will be automatically linked by CocoaPods.
// No need to manually add them here.
// AppTrackingTransparency is referenced by the Unity bridge, so add it explicitly and weak-link it for iOS 11+.
// - DirichletAdSDK.podspec: AdSupport, SystemConfiguration, Security
// - DirichletCoreSDK.podspec: SystemConfiguration, Security
// - DirichletMediationAdapterCSJ.podspec: CoreVideo
// - Third-party SDKs (Ads-CN, GDTMobSDK) declare their own framework dependencies.
pbxProject.AddFrameworkToProject(targetGuid, "AppTrackingTransparency.framework", true);
pbxProject.AddFrameworkToProject(targetGuid, "AdSupport.framework", true);
// Set build settings for framework target
pbxProject.SetBuildProperty(targetGuid, "ENABLE_BITCODE", "NO");
@@ -591,16 +616,16 @@ namespace Dirichlet.Mediation.Editor
// Note: The following Info.plist keys should be configured by the developer manually
// to avoid potential App Store review issues:
// - NSAppTransportSecurity: Configure based on your app's network requirements
// - NSUserTrackingUsageDescription: Required for iOS 14+ IDFA access, use your custom description
// - NSLocationWhenInUseUsageDescription: Only add if your app uses location services
//
// Reference: https://ssp.dirichlet.cn/docs/dirichlet-mediation-sdk/dirichlet-mediation-sdk-guide-ios/
// Add SKAdNetwork identifiers for attribution tracking
AddSKAdNetworkIds(rootDict);
AddTrackingUsageDescription(rootDict);
plist.WriteToFile(plistPath);
Debug.Log("[DirichletMediation] Modified Info.plist (SKAdNetwork IDs only)");
Debug.Log("[DirichletMediation] Modified Info.plist (SKAdNetwork IDs + ATT description)");
}
private static void AddSKAdNetworkIds(PlistElementDict rootDict)
@@ -631,6 +656,35 @@ namespace Dirichlet.Mediation.Editor
Debug.Log($"[DirichletMediation] Added {commonSkAdNetworkIds.Length} SKAdNetwork IDs to Info.plist");
}
private static void AddTrackingUsageDescription(PlistElementDict rootDict)
{
if (rootDict.values.ContainsKey("NSUserTrackingUsageDescription"))
{
Debug.Log("[DirichletMediation] NSUserTrackingUsageDescription already exists, skipping");
return;
}
rootDict.SetString("NSUserTrackingUsageDescription", ResolveTrackingUsageDescription());
Debug.Log("[DirichletMediation] Added NSUserTrackingUsageDescription to Info.plist");
}
private static string ResolveTrackingUsageDescription()
{
var envDescription = System.Environment.GetEnvironmentVariable(ENV_ATT_DESCRIPTION);
if (!string.IsNullOrWhiteSpace(envDescription))
{
return envDescription.Trim();
}
var prefDescription = EditorPrefs.GetString(PrefKeyATTDescription, string.Empty);
if (!string.IsNullOrWhiteSpace(prefDescription))
{
return prefDescription.Trim();
}
return DefaultTrackingUsageDescription;
}
private static void RunPodInstall(string projectPath)
{
var podfilePath = Path.Combine(projectPath, "Podfile");

View File

@@ -366,6 +366,10 @@ namespace Dirichlet.Mediation
private readonly IDirichletPlatformBridge bridge;
private static readonly object RewardAutoSessionLock = new object();
private static readonly Dictionary<string, AutoRewardVideoSession> RewardAutoSessions = new Dictionary<string, AutoRewardVideoSession>(StringComparer.Ordinal);
private static readonly object InterstitialAutoSessionLock = new object();
private static readonly Dictionary<string, AutoInterstitialSession> InterstitialAutoSessions = new Dictionary<string, AutoInterstitialSession>(StringComparer.Ordinal);
private static readonly object SplashAutoSessionLock = new object();
private static readonly Dictionary<string, AutoSplashSession> SplashAutoSessions = new Dictionary<string, AutoSplashSession>(StringComparer.Ordinal);
public static DirichletAdNative Create() => DirichletAdManager.CreateAdNative();
@@ -478,7 +482,7 @@ namespace Dirichlet.Mediation
/// <summary>
/// Shows an interstitial ad with automatic load-and-show logic.
/// Android only - iOS receives OnError with not_supported.
/// Android uses the native AutoAd API; iOS/editor use a load-then-show fallback without native cache.
/// </summary>
public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener)
{
@@ -487,13 +491,17 @@ namespace Dirichlet.Mediation
return;
}
#if UNITY_ANDROID && !UNITY_EDITOR
bridge.ShowInterstitialAutoAd(request, listener);
#else
ShowInterstitialLoadAndShowInternal(request, listener);
#endif
}
/// <summary>
/// Shows a banner ad with automatic load + rotation logic. Container is created internally and
/// anchored at the bottom of the screen by default. Use <see cref="DirichletAdRequest.Builder.WithSlideInterval"/>
/// to control rotation interval. Android only - iOS receives OnError with not_supported.
/// to control rotation interval. Native auto rotation is currently Android only.
/// </summary>
public void ShowBannerAutoAd(DirichletAdRequest request, IDirichletBannerAutoAdListener listener)
{
@@ -516,7 +524,7 @@ namespace Dirichlet.Mediation
/// <summary>
/// Shows a splash ad with automatic load logic. Container is a fullscreen overlay created internally.
/// Android only - iOS receives OnError with not_supported.
/// Android uses the native AutoAd API; iOS/editor use a load-then-show fallback without native cache.
/// </summary>
public void ShowSplashAutoAd(DirichletAdRequest request, IDirichletSplashAutoAdListener listener)
{
@@ -533,7 +541,11 @@ namespace Dirichlet.Mediation
return;
}
#if UNITY_ANDROID && !UNITY_EDITOR
bridge.ShowSplashAutoAd(request, options ?? new DirichletAdShowOptions(), listener);
#else
ShowSplashLoadAndShowInternal(request, options ?? new DirichletAdShowOptions(), listener);
#endif
}
/// <summary>
@@ -566,6 +578,7 @@ namespace Dirichlet.Mediation
var session = new AutoRewardVideoSession(sessionId, ad, listener);
RegisterRewardAutoSession(session);
ad.SetInteractionListener(session);
ad.ShowFailed += session.OnShowFailed;
// Load succeeded; show immediately.
var shown = ad.Show();
@@ -580,6 +593,66 @@ namespace Dirichlet.Mediation
});
}
private void ShowInterstitialLoadAndShowInternal(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener)
{
LoadInterstitialAd(
request,
ad =>
{
if (ad == null)
{
listener?.OnError(new DirichletError("invalid_ad", "Load callback returned null ad"));
return;
}
var sessionId = Guid.NewGuid().ToString("N");
var session = new AutoInterstitialSession(sessionId, ad, listener);
RegisterInterstitialAutoSession(session);
ad.SetInteractionListener(session);
ad.ShowFailed += session.OnShowFailed;
var shown = ad.Show();
if (!shown)
{
session.FailAndDispose(new DirichletError("show_failed", "ShowInterstitialAd returned false"));
}
},
error =>
{
listener?.OnError(error ?? new DirichletError("load_failed", "LoadInterstitialAd failed"));
});
}
private void ShowSplashLoadAndShowInternal(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener)
{
LoadSplashAd(
request,
ad =>
{
if (ad == null)
{
listener?.OnError(new DirichletError("invalid_ad", "Load callback returned null ad"));
return;
}
var sessionId = Guid.NewGuid().ToString("N");
var session = new AutoSplashSession(sessionId, ad, listener);
RegisterSplashAutoSession(session);
ad.SetInteractionListener(session);
ad.ShowFailed += session.OnShowFailed;
var shown = ad.Show();
if (!shown)
{
session.FailAndDispose(new DirichletError("show_failed", "ShowSplashAd returned false"));
}
},
error =>
{
listener?.OnError(error ?? new DirichletError("load_failed", "LoadSplashAd failed"));
});
}
private static bool ValidateRequest(DirichletAdRequest request, Action<DirichletError> onFailure)
{
if (request == null)
@@ -628,6 +701,58 @@ namespace Dirichlet.Mediation
}
}
private static void RegisterInterstitialAutoSession(AutoInterstitialSession session)
{
if (session == null)
{
return;
}
lock (InterstitialAutoSessionLock)
{
InterstitialAutoSessions[session.SessionId] = session;
}
}
private static void UnregisterInterstitialAutoSession(string sessionId)
{
if (string.IsNullOrEmpty(sessionId))
{
return;
}
lock (InterstitialAutoSessionLock)
{
InterstitialAutoSessions.Remove(sessionId);
}
}
private static void RegisterSplashAutoSession(AutoSplashSession session)
{
if (session == null)
{
return;
}
lock (SplashAutoSessionLock)
{
SplashAutoSessions[session.SessionId] = session;
}
}
private static void UnregisterSplashAutoSession(string sessionId)
{
if (string.IsNullOrEmpty(sessionId))
{
return;
}
lock (SplashAutoSessionLock)
{
SplashAutoSessions.Remove(sessionId);
}
}
private sealed class AutoRewardVideoSession : IDirichletRewardAdInteractionListener
{
public string SessionId { get; }
@@ -684,6 +809,11 @@ namespace Dirichlet.Mediation
listener?.OnRewardVerify(args);
}
public void OnShowFailed(DirichletError error)
{
FailAndDispose(error ?? new DirichletError("show_error", "Reward video ad failed to show"));
}
public void FailAndDispose(DirichletError error)
{
if (disposed)
@@ -707,6 +837,8 @@ namespace Dirichlet.Mediation
try
{
ad.ShowFailed -= OnShowFailed;
ad.SetInteractionListener(null);
ad?.Destroy();
}
catch (Exception ex)
@@ -715,6 +847,176 @@ namespace Dirichlet.Mediation
}
}
}
private sealed class AutoInterstitialSession : IDirichletInterstitialAdInteractionListener
{
public string SessionId { get; }
private readonly DirichletInterstitialAd ad;
private readonly IDirichletInterstitialAutoAdListener listener;
private bool disposed;
public AutoInterstitialSession(string sessionId, DirichletInterstitialAd ad, IDirichletInterstitialAutoAdListener listener)
{
SessionId = string.IsNullOrEmpty(sessionId) ? Guid.NewGuid().ToString("N") : sessionId;
this.ad = ad ?? throw new ArgumentNullException(nameof(ad));
this.listener = listener;
}
public void OnAdShow()
{
if (disposed)
{
return;
}
listener?.OnAdShow();
}
public void OnAdClick()
{
if (disposed)
{
return;
}
listener?.OnAdClick();
}
public void OnAdClose()
{
if (disposed)
{
return;
}
listener?.OnAdClose();
Dispose();
}
public void OnShowFailed(DirichletError error)
{
FailAndDispose(error ?? new DirichletError("show_error", "Interstitial ad failed to show"));
}
public void FailAndDispose(DirichletError error)
{
if (disposed)
{
return;
}
listener?.OnError(error ?? new DirichletError("unknown_error", "Unknown error"));
Dispose();
}
private void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
UnregisterInterstitialAutoSession(SessionId);
try
{
ad.ShowFailed -= OnShowFailed;
ad.SetInteractionListener(null);
ad?.Destroy();
}
catch (Exception ex)
{
Debug.LogWarning($"[Dirichlet] Auto interstitial ad Destroy failed: {ex.Message}");
}
}
}
private sealed class AutoSplashSession : IDirichletSplashAdInteractionListener
{
public string SessionId { get; }
private readonly DirichletSplashAd ad;
private readonly IDirichletSplashAutoAdListener listener;
private bool disposed;
public AutoSplashSession(string sessionId, DirichletSplashAd ad, IDirichletSplashAutoAdListener listener)
{
SessionId = string.IsNullOrEmpty(sessionId) ? Guid.NewGuid().ToString("N") : sessionId;
this.ad = ad ?? throw new ArgumentNullException(nameof(ad));
this.listener = listener;
}
public void OnAdShow()
{
if (disposed)
{
return;
}
listener?.OnAdShow();
}
public void OnAdClick()
{
if (disposed)
{
return;
}
listener?.OnAdClick();
}
public void OnAdClose()
{
if (disposed)
{
return;
}
listener?.OnAdClose();
Dispose();
}
public void OnShowFailed(DirichletError error)
{
FailAndDispose(error ?? new DirichletError("show_error", "Splash ad failed to show"));
}
public void FailAndDispose(DirichletError error)
{
if (disposed)
{
return;
}
listener?.OnError(error ?? new DirichletError("unknown_error", "Unknown error"));
Dispose();
}
private void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
UnregisterSplashAutoSession(SessionId);
try
{
ad.ShowFailed -= OnShowFailed;
ad.SetInteractionListener(null);
ad?.Destroy();
}
catch (Exception ex)
{
Debug.LogWarning($"[Dirichlet] Auto splash ad Destroy failed: {ex.Message}");
}
}
}
}
/// <summary>
@@ -1189,7 +1491,7 @@ namespace Dirichlet.Mediation
/// <summary>
/// Listener interface for auto interstitial ad callbacks.
/// Android only - iOS will receive OnError with not_supported error.
/// Android uses native auto cache; iOS/editor can emulate load-then-show.
/// </summary>
public interface IDirichletInterstitialAutoAdListener
{
@@ -1201,7 +1503,7 @@ namespace Dirichlet.Mediation
/// <summary>
/// Listener interface for auto banner ad callbacks.
/// Android only - iOS will receive OnError with not_supported error.
/// Native auto rotation is Android only.
/// </summary>
public interface IDirichletBannerAutoAdListener
{
@@ -1213,7 +1515,7 @@ namespace Dirichlet.Mediation
/// <summary>
/// Listener interface for auto splash ad callbacks.
/// Android only - iOS will receive OnError with not_supported error.
/// Android uses native auto cache; iOS/editor can emulate load-then-show.
/// </summary>
public interface IDirichletSplashAutoAdListener
{
@@ -1226,7 +1528,7 @@ namespace Dirichlet.Mediation
/// <summary>
/// Listener interface for auto reward video ad callbacks.
/// Used with ShowRewardVideoAutoAd which combines load and show into one operation.
/// Android only - iOS will receive OnError with not_supported error.
/// Android uses native auto cache; iOS/editor can emulate load-then-show.
/// </summary>
public interface IDirichletRewardVideoAutoAdListener
{

View File

@@ -580,12 +580,12 @@ namespace Dirichlet.Mediation
/// <summary>
/// Shows a reward video ad with automatic load-and-show logic.
/// Android only - iOS will call onFailure with not_supported error.
/// Android bridge maps to native auto cache; higher layers may emulate load-then-show elsewhere.
/// </summary>
void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener);
/// <summary>
/// Shows an interstitial ad with automatic load-and-show logic. Android only.
/// Shows an interstitial ad with automatic load-and-show logic. Android bridge maps to native auto cache.
/// </summary>
void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener);
@@ -595,7 +595,7 @@ namespace Dirichlet.Mediation
void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener);
/// <summary>
/// Shows a splash ad with automatic load logic. Android only.
/// Shows a splash ad with automatic load logic. Android bridge maps to native auto cache.
/// </summary>
void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener);
@@ -1563,20 +1563,9 @@ namespace Dirichlet.Mediation
}
first = false;
jsonBuilder.Append($"\"{kv.Key}\":");
if (kv.Value is string)
{
jsonBuilder.Append($"\"{kv.Value}\"");
}
else if (kv.Value is bool)
{
jsonBuilder.Append(((bool)kv.Value) ? "true" : "false");
}
else
{
jsonBuilder.Append(kv.Value.ToString());
}
AppendJsonString(jsonBuilder, kv.Key);
jsonBuilder.Append(":");
AppendJsonValue(jsonBuilder, kv.Value);
}
jsonBuilder.Append("}");
@@ -1589,6 +1578,84 @@ namespace Dirichlet.Mediation
}
}
private static void AppendJsonValue(System.Text.StringBuilder builder, object value)
{
if (value == null)
{
builder.Append("null");
return;
}
if (value is string stringValue)
{
AppendJsonString(builder, stringValue);
return;
}
if (value is bool boolValue)
{
builder.Append(boolValue ? "true" : "false");
return;
}
if (value is IFormattable formattable)
{
builder.Append(formattable.ToString(null, CultureInfo.InvariantCulture));
return;
}
AppendJsonString(builder, value.ToString());
}
private static void AppendJsonString(System.Text.StringBuilder builder, string value)
{
builder.Append('"');
if (!string.IsNullOrEmpty(value))
{
foreach (var c in value)
{
switch (c)
{
case '\\':
builder.Append("\\\\");
break;
case '"':
builder.Append("\\\"");
break;
case '\b':
builder.Append("\\b");
break;
case '\f':
builder.Append("\\f");
break;
case '\n':
builder.Append("\\n");
break;
case '\r':
builder.Append("\\r");
break;
case '\t':
builder.Append("\\t");
break;
default:
if (char.IsControl(c))
{
builder.Append("\\u");
builder.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
}
else
{
builder.Append(c);
}
break;
}
}
}
builder.Append('"');
}
private void RemoveLoadCallback(string handleId)
{
if (string.IsNullOrEmpty(handleId))
@@ -2032,5 +2099,3 @@ namespace Dirichlet.Mediation
#endregion
}

View File

@@ -10,6 +10,7 @@
#import "DirichletMediationUnityBridge.h"
#import <DirichletMediationSDK/DirichletMediationSDK.h>
#import <UIKit/UIKit.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
// Unity callback interface
extern "C" void UnitySendMessage(const char* obj, const char* method, const char* msg);
@@ -119,6 +120,88 @@ static NSDictionary* ParseExtras(const char* extrasJson) {
return error ? nil : dict;
}
static void SendLoadErrorToUnity(NSString* handleId, NSString* adType, NSError* error, NSString* fallbackMessage) {
NSInteger code = error ? error.code : -1;
NSString* message = error.localizedDescription ?: fallbackMessage ?: @"Unknown error";
NSDictionary* errorData = @{
@"code": @(code),
@"message": message
};
SendLoadCallbackToUnity(handleId, @"load_error", adType, errorData);
}
static void SendShowErrorToUnity(NSString* handleId, NSString* adType, NSInteger code, NSString* message) {
NSDictionary* errorData = @{
@"code": @(code),
@"message": message ?: @"Ad failed to show"
};
SendEventToUnity(handleId, @"show_error", adType ?: @"unknown", errorData);
}
static UIViewController* TopViewController(UIViewController* rootViewController) {
UIViewController* top = rootViewController;
while (top.presentedViewController) {
top = top.presentedViewController;
}
if ([top isKindOfClass:[UINavigationController class]]) {
return TopViewController(((UINavigationController*)top).visibleViewController);
}
if ([top isKindOfClass:[UITabBarController class]]) {
return TopViewController(((UITabBarController*)top).selectedViewController);
}
return top;
}
static UIViewController* CurrentRootViewController(void) {
UIWindow* keyWindow = nil;
if (@available(iOS 13.0, *)) {
for (UIScene* scene in [UIApplication sharedApplication].connectedScenes) {
if (scene.activationState != UISceneActivationStateForegroundActive ||
![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
UIWindowScene* windowScene = (UIWindowScene*)scene;
for (UIWindow* window in windowScene.windows) {
if (window.isKeyWindow) {
keyWindow = window;
break;
}
}
if (keyWindow) {
break;
}
}
}
if (!keyWindow) {
keyWindow = [UIApplication sharedApplication].keyWindow;
}
return TopViewController(keyWindow.rootViewController);
}
static NSString* AdTypeForObject(id ad) {
if ([ad isKindOfClass:[DRMRewardVideoAd class]]) {
return @"reward_video";
}
if ([ad isKindOfClass:[DRMInterstitialAd class]]) {
return @"interstitial";
}
if ([ad isKindOfClass:[DRMBannerAd class]]) {
return @"banner";
}
if ([ad isKindOfClass:[DRMSplashAd class]]) {
return @"splash";
}
return @"unknown";
}
#pragma mark - Ad Instance Manager
@interface DirichletMediationInstanceManager : NSObject
@@ -336,6 +419,7 @@ bool DirichletMediationUnityBridge_Initialize(
// Check if SDK is already initialized
if ([DirichletMediation isInitialized]) {
NSLog(@"[DirichletMediationUnityBridge] SDK already initialized");
SendInitCallbackToUnity(YES, nil, @"already_initialized");
return true;
}
@@ -388,8 +472,29 @@ bool DirichletMediationUnityBridge_Initialize(
}
void DirichletMediationUnityBridge_RequestPermissionIfNeeded(void) {
// iOS 14+ ATT permission is handled internally by the SDK
NSLog(@"[DirichletMediationUnityBridge] RequestPermissionIfNeeded called");
if (@available(iOS 14.0, *)) {
void (^requestBlock)(void) = ^{
ATTrackingManagerAuthorizationStatus status = [ATTrackingManager trackingAuthorizationStatus];
if (status != ATTrackingManagerAuthorizationStatusNotDetermined) {
NSLog(@"[DirichletMediationUnityBridge] ATT status already determined: %lu", (unsigned long)status);
return;
}
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
NSLog(@"[DirichletMediationUnityBridge] ATT request completed with status: %lu", (unsigned long)status);
}];
};
if ([NSThread isMainThread]) {
requestBlock();
} else {
dispatch_async(dispatch_get_main_queue(), requestBlock);
}
} else {
NSLog(@"[DirichletMediationUnityBridge] ATT is not required below iOS 14");
}
}
const char* DirichletMediationUnityBridge_GetSdkVersion(void) {
@@ -438,11 +543,7 @@ const char* DirichletMediationUnityBridge_LoadRewardVideoAd(long long spaceId, c
SendLoadCallbackToUnity(handleId, @"load_success", @"reward_video", nil);
} else {
NSLog(@"[DirichletMediationUnityBridge] RewardVideoAd load failed: %@", error.localizedDescription);
NSDictionary* errorData = @{
@"code": @(error.code),
@"message": error.localizedDescription ?: @"Unknown error"
};
SendLoadCallbackToUnity(handleId, @"load_error", @"reward_video", errorData);
SendLoadErrorToUnity(handleId, @"reward_video", error, @"RewardVideoAd load failed");
}
}];
@@ -475,11 +576,7 @@ const char* DirichletMediationUnityBridge_LoadInterstitialAd(long long spaceId,
SendLoadCallbackToUnity(handleId, @"load_success", @"interstitial", nil);
} else {
NSLog(@"[DirichletMediationUnityBridge] InterstitialAd load failed: %@", error.localizedDescription);
NSDictionary* errorData = @{
@"code": @(error.code),
@"message": error.localizedDescription ?: @"Unknown error"
};
SendLoadCallbackToUnity(handleId, @"load_error", @"interstitial", errorData);
SendLoadErrorToUnity(handleId, @"interstitial", error, @"InterstitialAd load failed");
}
}];
@@ -520,11 +617,7 @@ const char* DirichletMediationUnityBridge_LoadBannerAd(long long spaceId, const
SendLoadCallbackToUnity(handleId, @"load_success", @"banner", nil);
} else {
NSLog(@"[DirichletMediationUnityBridge] BannerAd load failed: %@", error.localizedDescription);
NSDictionary* errorData = @{
@"code": @(error.code),
@"message": error.localizedDescription ?: @"Unknown error"
};
SendLoadCallbackToUnity(handleId, @"load_error", @"banner", errorData);
SendLoadErrorToUnity(handleId, @"banner", error, @"BannerAd load failed");
}
}];
@@ -565,11 +658,7 @@ const char* DirichletMediationUnityBridge_LoadSplashAd(long long spaceId, const
SendLoadCallbackToUnity(handleId, @"load_success", @"splash", nil);
} else {
NSLog(@"[DirichletMediationUnityBridge] SplashAd load failed: %@", error.localizedDescription);
NSDictionary* errorData = @{
@"code": @(error.code),
@"message": error.localizedDescription ?: @"Unknown error"
};
SendLoadCallbackToUnity(handleId, @"load_error", @"splash", errorData);
SendLoadErrorToUnity(handleId, @"splash", error, @"SplashAd load failed");
}
}];
@@ -585,11 +674,15 @@ bool DirichletMediationUnityBridge_ShowAd(const char* handleId, const char* extr
return false;
}
// Ensure show is always called on main thread (aligned with Ad Unity implementation)
dispatch_async(dispatch_get_main_queue(), ^{
UIViewController* rootVC = [[[UIApplication sharedApplication] keyWindow] rootViewController];
__block BOOL didStartShow = NO;
NSString* adType = AdTypeForObject(ad);
NSDictionary* extrasDict = ParseExtras(extras);
void (^showBlock)(void) = ^{
UIViewController* rootVC = CurrentRootViewController();
if (!rootVC) {
NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Root view controller not found");
SendShowErrorToUnity(nsHandleId, adType, -2, @"Root view controller not found");
return;
}
@@ -597,53 +690,87 @@ bool DirichletMediationUnityBridge_ShowAd(const char* handleId, const char* extr
DRMRewardVideoAd* rewardAd = (DRMRewardVideoAd*)ad;
if ([rewardAd isReady]) {
[rewardAd showFromViewController:rootVC];
didStartShow = YES;
NSLog(@"[DirichletMediationUnityBridge] Showing reward video ad: %@", nsHandleId);
} else {
SendShowErrorToUnity(nsHandleId, @"reward_video", -3, @"Reward video ad is not ready");
}
} else if ([ad isKindOfClass:[DRMInterstitialAd class]]) {
DRMInterstitialAd* interstitialAd = (DRMInterstitialAd*)ad;
if ([interstitialAd isReady]) {
[interstitialAd showFromViewController:rootVC];
didStartShow = YES;
NSLog(@"[DirichletMediationUnityBridge] Showing interstitial ad: %@", nsHandleId);
} else {
SendShowErrorToUnity(nsHandleId, @"interstitial", -3, @"Interstitial ad is not ready");
}
} else if ([ad isKindOfClass:[DRMBannerAd class]]) {
DRMBannerAd* bannerAd = (DRMBannerAd*)ad;
UIView* bannerView = bannerAd.view;
if (bannerView) {
// Banner 广告需要将 view 添加到视图控制器上
// 注意Unity 侧需要通过 Unity UI 系统来处理 Banner 视图
// 这里我们发送一个事件通知 Unity 侧,让 Unity 侧来处理视图的展示
// 或者直接将视图添加到根视图控制器上(临时方案)
[bannerView removeFromSuperview];
[rootVC.view addSubview:bannerView];
bannerView.translatesAutoresizingMaskIntoConstraints = NO;
// 设置约束,让 Banner 显示在底部
NSInteger baseline = [extrasDict[@"banner_baseline"] integerValue];
CGFloat offset = extrasDict[@"banner_offset"] ? [extrasDict[@"banner_offset"] floatValue] : 0;
NSLayoutYAxisAnchor* verticalAnchor = baseline == 0
? rootVC.view.safeAreaLayoutGuide.topAnchor
: rootVC.view.safeAreaLayoutGuide.bottomAnchor;
NSLayoutConstraint* verticalConstraint = baseline == 0
? [bannerView.topAnchor constraintEqualToAnchor:verticalAnchor constant:offset]
: [bannerView.bottomAnchor constraintEqualToAnchor:verticalAnchor constant:-offset];
[NSLayoutConstraint activateConstraints:@[
[bannerView.leadingAnchor constraintEqualToAnchor:rootVC.view.leadingAnchor],
[bannerView.trailingAnchor constraintEqualToAnchor:rootVC.view.trailingAnchor],
[bannerView.bottomAnchor constraintEqualToAnchor:rootVC.view.safeAreaLayoutGuide.bottomAnchor],
[bannerView.centerXAnchor constraintEqualToAnchor:rootVC.view.centerXAnchor],
[bannerView.widthAnchor constraintLessThanOrEqualToAnchor:rootVC.view.widthAnchor],
verticalConstraint,
[bannerView.heightAnchor constraintEqualToConstant:bannerAd.size.height > 0 ? bannerAd.size.height : 50]
]];
didStartShow = YES;
NSLog(@"[DirichletMediationUnityBridge] Showing banner ad: %@", nsHandleId);
} else {
NSLog(@"[DirichletMediationUnityBridge] Banner ad view not available: %@", nsHandleId);
SendShowErrorToUnity(nsHandleId, @"banner", -4, @"Banner ad view not available");
}
} else if ([ad isKindOfClass:[DRMSplashAd class]]) {
DRMSplashAd* splashAd = (DRMSplashAd*)ad;
if ([splashAd isReady]) {
[splashAd showFromViewController:rootVC];
didStartShow = YES;
NSLog(@"[DirichletMediationUnityBridge] Showing splash ad: %@", nsHandleId);
} else {
SendShowErrorToUnity(nsHandleId, @"splash", -3, @"Splash ad is not ready");
}
} else {
NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Ad not ready or unknown type");
SendShowErrorToUnity(nsHandleId, @"unknown", -5, @"Unknown ad type");
}
});
};
return true;
if ([NSThread isMainThread]) {
showBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), showBlock);
}
return didStartShow;
}
void DirichletMediationUnityBridge_DestroyAd(const char* handleId) {
NSString* nsHandleId = CreateNSString(handleId);
NSLog(@"[DirichletMediationUnityBridge] Destroying ad: %@", nsHandleId);
id ad = [[DirichletMediationInstanceManager shared] adForHandle:nsHandleId];
if ([ad isKindOfClass:[DRMBannerAd class]]) {
DRMBannerAd* bannerAd = (DRMBannerAd*)ad;
if (bannerAd.view.superview) {
dispatch_async(dispatch_get_main_queue(), ^{
[bannerAd.view removeFromSuperview];
});
}
}
// Remove ad instance
[[DirichletMediationInstanceManager shared] removeAdForHandle:nsHandleId];
@@ -679,4 +806,3 @@ bool DirichletMediationUnityBridge_IsAdValid(const char* handleId) {
}
} // extern "C"