From 238c5fa11f60d7d524c7a28337090a8c0d3c08f4 Mon Sep 17 00:00:00 2001 From: bryanboyko Date: Fri, 17 Apr 2026 10:51:34 -0700 Subject: [PATCH 1/2] feat(demo-apps): conform to CLXTestHarnessApp settings-mutation API Adds -applySettingsMutation:params:error: and -verifyCleanState: conformance in both demo apps so the QA harness can drive the settings-mutation matrix without XCUITest. Covers gdpr_applies, us_privacy, hashed_user_id, user_kv_add, app_kv_add, clear_all_kvs, user_targeting_off, and hi_roi_targeting_signals by calling CloudXCore's public privacy and key-value APIs plus the IAB NSUserDefaults keys for GDPR/CCPA signals. verifyCleanState inspects CLXManualPrivacyState, CLXKeyValueState, and the IAB NSUserDefaults keys so no state leaks between mutation rows. Requires CloudXCore 3.1.0 or newer for the CLXKeyValueState / CLXManualPrivacyState accessors; the demo apps are already pinned to 3.1.0 on main. Paired with the CLXTestHarnessApp protocol additions in the cloudx-qa-automation feature branch. Made-with: Cursor --- .../CloudXObjCRemotePods/CLXDeepLinkRouter.m | 139 ++++++++++++++++++ .../DeepLinkRouter.swift | 97 ++++++++++++ 2 files changed, 236 insertions(+) diff --git a/demo-app-objc/CloudXObjCRemotePods/CLXDeepLinkRouter.m b/demo-app-objc/CloudXObjCRemotePods/CLXDeepLinkRouter.m index 21d30ef7..24272ae9 100644 --- a/demo-app-objc/CloudXObjCRemotePods/CLXDeepLinkRouter.m +++ b/demo-app-objc/CloudXObjCRemotePods/CLXDeepLinkRouter.m @@ -6,6 +6,10 @@ #import "NativeViewController.h" #import "DemoAppLogger.h" #import +#import +#import +#import +#import static NSTimeInterval const kSDKInitTimeout = 30.0; @@ -267,6 +271,141 @@ - (nullable UIWindow *)keyWindow { return nil; } +#pragma mark - Settings Mutation (CLXTestHarnessApp optional) + +static NSError *clx_mutationError(NSString *reason) { + return [NSError errorWithDomain:@"CLXDemoDeepLinkRouter" + code:1001 + userInfo:@{ NSLocalizedDescriptionKey: reason ?: @"unknown" }]; +} + +- (BOOL)applySettingsMutation:(NSString *)mutationId + params:(NSDictionary *)params + error:(NSError *__autoreleasing *)error { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + CloudXCore *core = [CloudXCore shared]; + + if ([mutationId isEqualToString:@"gdpr_applies"]) { + NSNumber *applies = params[@"applies"] ?: @YES; + NSString *consent = params[@"consent"] ?: @"CPabc"; + [defaults setObject:applies forKey:kCLXPrivacyGDPRAppliesKey]; + [defaults setObject:consent forKey:kCLXPrivacyGDPRConsentKey]; + [CloudXCore setHasUserConsent:@YES]; + return YES; + } + if ([mutationId isEqualToString:@"us_privacy"]) { + NSString *value = params[@"value"] ?: @"1YNN"; + [defaults setObject:value forKey:kCLXPrivacyCCPAPrivacyKey]; + return YES; + } + if ([mutationId isEqualToString:@"hashed_user_id"]) { + NSString *value = params[@"value"]; + if (![value isKindOfClass:[NSString class]] || value.length == 0) { + if (error) *error = clx_mutationError(@"hashed_user_id requires non-empty value"); + return NO; + } + [core setHashedUserID:value]; + return YES; + } + if ([mutationId isEqualToString:@"user_kv_add"]) { + NSString *key = params[@"key"]; + NSString *value = params[@"value"]; + if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) { + if (error) *error = clx_mutationError(@"user_kv_add requires string key and value"); + return NO; + } + [core setUserKeyValue:key value:value]; + return YES; + } + if ([mutationId isEqualToString:@"app_kv_add"]) { + NSString *key = params[@"key"]; + NSString *value = params[@"value"]; + if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) { + if (error) *error = clx_mutationError(@"app_kv_add requires string key and value"); + return NO; + } + [core setAppKeyValue:key value:value]; + return YES; + } + if ([mutationId isEqualToString:@"clear_all_kvs"]) { + [core clearAllKeyValues]; + return YES; + } + if ([mutationId isEqualToString:@"user_targeting_off"]) { + // No first-class API yet; clear user KVs as the observable equivalent + // so user.ext.data / demographics are absent on the next bid request. + [core clearAllKeyValues]; + return YES; + } + if ([mutationId isEqualToString:@"hi_roi_targeting_signals"]) { + NSDictionary *userKVs = params[@"user"] ?: @{ + @"ltv_bucket": @"high", + @"retention_d7": @"true", + }; + NSDictionary *appKVs = params[@"app"] ?: @{ + @"content_rating": @"E", + @"monetization_tier": @"premium", + }; + for (NSString *key in userKVs) { + id value = userKVs[key]; + if ([value isKindOfClass:[NSString class]]) { + [core setUserKeyValue:key value:(NSString *)value]; + } + } + for (NSString *key in appKVs) { + id value = appKVs[key]; + if ([value isKindOfClass:[NSString class]]) { + [core setAppKeyValue:key value:(NSString *)value]; + } + } + return YES; + } + + if (error) *error = clx_mutationError([NSString stringWithFormat:@"unsupported mutationId: %@", mutationId]); + return NO; +} + +- (BOOL)verifyCleanState:(NSError *__autoreleasing *)error { + CLXManualPrivacyState *privacy = [CLXManualPrivacyState sharedInstance]; + if (privacy.hasUserConsent != nil) { + if (error) *error = clx_mutationError(@"CLXManualPrivacyState.hasUserConsent is set"); + return NO; + } + if (privacy.doNotSell != nil) { + if (error) *error = clx_mutationError(@"CLXManualPrivacyState.doNotSell is set"); + return NO; + } + + CLXKeyValueState *kvs = [CLXKeyValueState shared]; + if (kvs.hashedUserId.length > 0) { + if (error) *error = clx_mutationError(@"CLXKeyValueState.hashedUserId is set"); + return NO; + } + if (kvs.userKeyValues.count > 0) { + if (error) *error = clx_mutationError(@"CLXKeyValueState.userKeyValues is non-empty"); + return NO; + } + if (kvs.appKeyValues.count > 0) { + if (error) *error = clx_mutationError(@"CLXKeyValueState.appKeyValues is non-empty"); + return NO; + } + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSArray *iabKeys = @[ + kCLXPrivacyGDPRAppliesKey, + kCLXPrivacyGDPRConsentKey, + kCLXPrivacyCCPAPrivacyKey, + ]; + for (NSString *key in iabKeys) { + if ([defaults objectForKey:key] != nil) { + if (error) *error = clx_mutationError([NSString stringWithFormat:@"NSUserDefaults %@ is set", key]); + return NO; + } + } + + return YES; +} + @end #pragma mark - CLXDeepLinkRouter (Public API) diff --git a/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift b/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift index 55b1cb0a..14d4cbb7 100644 --- a/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift +++ b/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift @@ -2,6 +2,7 @@ import UIKit #if canImport(CloudXTestHarness) import CloudXTestHarness +import CloudXCore private let kSDKInitTimeout: TimeInterval = 30.0 @@ -238,6 +239,102 @@ private final class Adapter: NSObject, CLXTestHarnessApp { .flatMap { $0.windows } .first { $0.isKeyWindow } } + + // MARK: - Settings Mutation (CLXTestHarnessApp optional) + + private static func mutationError(_ reason: String) -> NSError { + NSError(domain: "CLXDemoDeepLinkRouter", + code: 1001, + userInfo: [NSLocalizedDescriptionKey: reason]) + } + + func applySettingsMutation(_ mutationId: String, + params: [AnyHashable: Any]) throws { + let defaults = UserDefaults.standard + let core = CloudXCore.shared() + + switch mutationId { + case "gdpr_applies": + let applies = params["applies"] as? NSNumber ?? NSNumber(value: true) + let consent = (params["consent"] as? String) ?? "CPabc" + defaults.set(applies, forKey: "IABTCF_gdprApplies") + defaults.set(consent, forKey: "IABTCF_TCString") + CloudXCore.setHasUserConsent(NSNumber(value: true)) + + case "us_privacy": + let value = (params["value"] as? String) ?? "1YNN" + defaults.set(value, forKey: "IABUSPrivacy_String") + + case "hashed_user_id": + guard let value = params["value"] as? String, !value.isEmpty else { + throw Adapter.mutationError("hashed_user_id requires non-empty value") + } + core.setHashedUserID(value) + + case "user_kv_add": + guard let key = params["key"] as? String, + let value = params["value"] as? String else { + throw Adapter.mutationError("user_kv_add requires string key and value") + } + core.setUserKeyValue(key, value: value) + + case "app_kv_add": + guard let key = params["key"] as? String, + let value = params["value"] as? String else { + throw Adapter.mutationError("app_kv_add requires string key and value") + } + core.setAppKeyValue(key, value: value) + + case "clear_all_kvs": + core.clearAllKeyValues() + + case "user_targeting_off": + // No first-class API yet; clear user KVs as the observable equivalent. + core.clearAllKeyValues() + + case "hi_roi_targeting_signals": + let userKVs = (params["user"] as? [String: String]) ?? [ + "ltv_bucket": "high", + "retention_d7": "true", + ] + let appKVs = (params["app"] as? [String: String]) ?? [ + "content_rating": "E", + "monetization_tier": "premium", + ] + for (key, value) in userKVs { core.setUserKeyValue(key, value: value) } + for (key, value) in appKVs { core.setAppKeyValue(key, value: value) } + + default: + throw Adapter.mutationError("unsupported mutationId: \(mutationId)") + } + } + + func verifyCleanState() throws { + let privacy = CLXManualPrivacyState.sharedInstance() + if privacy.hasUserConsent != nil { + throw Adapter.mutationError("CLXManualPrivacyState.hasUserConsent is set") + } + if privacy.doNotSell != nil { + throw Adapter.mutationError("CLXManualPrivacyState.doNotSell is set") + } + + let kvs = CLXKeyValueState.shared() + if let hashed = kvs.hashedUserId, !hashed.isEmpty { + throw Adapter.mutationError("CLXKeyValueState.hashedUserId is set") + } + if kvs.userKeyValues.count > 0 { + throw Adapter.mutationError("CLXKeyValueState.userKeyValues is non-empty") + } + if kvs.appKeyValues.count > 0 { + throw Adapter.mutationError("CLXKeyValueState.appKeyValues is non-empty") + } + + let iabKeys = ["IABTCF_gdprApplies", "IABTCF_TCString", "IABUSPrivacy_String"] + let defaults = UserDefaults.standard + for key in iabKeys where defaults.object(forKey: key) != nil { + throw Adapter.mutationError("UserDefaults \(key) is set") + } + } } #else From 6665a5986c5f969a1893878478419eba5f9f97f0 Mon Sep 17 00:00:00 2001 From: bryanboyko Date: Fri, 17 Apr 2026 12:32:02 -0700 Subject: [PATCH 2/2] CXD-1045: Fix Swift build + opt-in CLX_QA_HARNESS podfile entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeepLinkRouter.swift used `CloudXCore.shared()` which fails to compile — the ObjC `+ (instancetype)shared` bridges as a Swift property, not a method. Changed to `CloudXCore.shared`. Both demo Podfiles gain an opt-in `pod 'CloudXTestHarness'` line guarded by `ENV['CLX_QA_HARNESS'] == '1'`. Normal dev and release builds are unchanged (harness absent). The QA orchestrator sets the env var before `pod install` so the adapter's `applySettingsMutation:` and `verifyCleanState:` conformances get wired through the harness. Podfile.lock updates reflect the existing `~> 3.1.0` pinning that was already on `main` — no new SDK version bump. Validated: both demo apps compile clean against CloudXCore 3.1.0 with the harness linked locally. Made-with: Cursor --- demo-app-objc/Podfile | 8 +++ demo-app-objc/Podfile.lock | 60 +++++++++---------- .../DeepLinkRouter.swift | 2 +- demo-app-swift/Podfile | 8 +++ demo-app-swift/Podfile.lock | 60 +++++++++---------- 5 files changed, 77 insertions(+), 61 deletions(-) diff --git a/demo-app-objc/Podfile b/demo-app-objc/Podfile index 9cc6d247..e79b541f 100644 --- a/demo-app-objc/Podfile +++ b/demo-app-objc/Podfile @@ -11,6 +11,14 @@ target 'CloudXObjCRemotePods' do pod 'CloudXMintegralAdapter', '~> 3.1.0' pod 'CloudXUnityAdsAdapter', '~> 3.1.0' + # Local QA harness. Enabled only when CLX_QA_HARNESS=1 so normal dev/release + # builds remain unchanged. The demo app's CLXDeepLinkRouter is guarded by + # __has_include() and falls back to + # no-op stubs when the harness is absent. + if ENV['CLX_QA_HARNESS'] == '1' + pod 'CloudXTestHarness', :path => '../../cloudx-qa-automation/sdk/ios/CloudXTestHarness' + end + target 'CloudXObjCRemotePodsTests' do inherit! :search_paths end diff --git a/demo-app-objc/Podfile.lock b/demo-app-objc/Podfile.lock index 44f4a3d1..a9bb05f4 100644 --- a/demo-app-objc/Podfile.lock +++ b/demo-app-objc/Podfile.lock @@ -1,24 +1,24 @@ PODS: - - CloudXCore (2.2.9) - - CloudXInMobiAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXCore (3.1.0) + - CloudXInMobiAdapter (3.1.0): + - CloudXCore (= 3.1.0) - InMobiSDK (< 12.0, >= 11.0.0) - - CloudXMetaAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXMetaAdapter (3.1.0): + - CloudXCore (= 3.1.0) - FBAudienceNetwork (< 7.0, >= 6.9.0) - - CloudXMintegralAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXMintegralAdapter (3.1.0): + - CloudXCore (= 3.1.0) - MintegralAdSDK (~> 8.0) - MintegralAdSDK/BidBannerAd (~> 8.0) - MintegralAdSDK/BidNewInterstitialAd (~> 8.0) - MintegralAdSDK/BidRewardVideoAd (~> 8.0) - - CloudXRenderer (2.2.9): - - CloudXCore (= 2.2.9) - - CloudXUnityAdsAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXRenderer (3.1.0): + - CloudXCore (= 3.1.0) + - CloudXUnityAdsAdapter (3.1.0): + - CloudXCore (= 3.1.0) - UnityAds (~> 4.17.0) - - CloudXVungleAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXVungleAdapter (3.1.0): + - CloudXCore (= 3.1.0) - VungleAds (< 8.0, >= 7.4.0) - FBAudienceNetwork (6.21.1) - InMobiSDK (11.2.0) @@ -58,16 +58,16 @@ PODS: - MintegralAdSDK/RewardVideoAd (8.1.1): - MintegralAdSDK/NativeAd - UnityAds (4.17.0) - - VungleAds (7.7.1) + - VungleAds (7.7.2) DEPENDENCIES: - - CloudXCore (~> 2.2.9) - - CloudXInMobiAdapter (~> 2.2.9) - - CloudXMetaAdapter (~> 2.2.9) - - CloudXMintegralAdapter (~> 2.2.9) - - CloudXRenderer (~> 2.2.9) - - CloudXUnityAdsAdapter (~> 2.2.9) - - CloudXVungleAdapter (~> 2.2.9) + - CloudXCore (~> 3.1.0) + - CloudXInMobiAdapter (~> 3.1.0) + - CloudXMetaAdapter (~> 3.1.0) + - CloudXMintegralAdapter (~> 3.1.0) + - CloudXRenderer (~> 3.1.0) + - CloudXUnityAdsAdapter (~> 3.1.0) + - CloudXVungleAdapter (~> 3.1.0) SPEC REPOS: trunk: @@ -85,19 +85,19 @@ SPEC REPOS: - VungleAds SPEC CHECKSUMS: - CloudXCore: 409c559936a63bb1364cf85237e72129da03ecfc - CloudXInMobiAdapter: 28c1f8cea73c480c3e03fbd0b04a813b3e4f5a1a - CloudXMetaAdapter: c50d3e86b24a6cd82b552cea58a99cec7fa00e4f - CloudXMintegralAdapter: 6f3b5dcf1bdf7d388a72d8caec40bd877d8f3dda - CloudXRenderer: 8e37cd5201c2d410a1f84088c51195a5aa29f11a - CloudXUnityAdsAdapter: 0eadb3278c5e2e0211bd6a86d4bc4a4f442c2e81 - CloudXVungleAdapter: 78e4dd5e3216bb881c02cf8615bbf98e52717447 + CloudXCore: 4ec790b817d818e50b485289b832116f7da4ab25 + CloudXInMobiAdapter: 5f2bbfb2f146dd2466592bdc59334cf8cb9b53a3 + CloudXMetaAdapter: a15e48c9ce02d518db49578145a0a2574820e5fa + CloudXMintegralAdapter: 1c92ec727cfad8ae2a6ba5227dba51aaed2a8052 + CloudXRenderer: 56ad15a24c7cf56280f5c2344d2304330668d857 + CloudXUnityAdsAdapter: 270501625d4560e539e157cf8d56efa514959b80 + CloudXVungleAdapter: d82770f30054897fc6fdc444192c20c3e3b5c9ab FBAudienceNetwork: 85d18a65c9e35bc426bd4664cf3ae2a1172d7b60 InMobiSDK: 07312ef7eff433d72e8a0f06ef7b50685b3e2ca5 MintegralAdSDK: 4325769f93ef336237799b5fb455335985fbc8b7 UnityAds: adbe0d522fd14b8298c5e159164fc43396165843 - VungleAds: c1f0dd0d8531b200b9f6a3f78e292ded75540eae + VungleAds: 03d93126f39d6056055a8b9edca701c6a3391d8b -PODFILE CHECKSUM: f44e03398d69dbdaef048b1249c4b3351e8372b2 +PODFILE CHECKSUM: 95af828f61d247bfba70c56ffdf876884d4436bd COCOAPODS: 1.16.2 diff --git a/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift b/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift index 14d4cbb7..fd49c0c6 100644 --- a/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift +++ b/demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift @@ -251,7 +251,7 @@ private final class Adapter: NSObject, CLXTestHarnessApp { func applySettingsMutation(_ mutationId: String, params: [AnyHashable: Any]) throws { let defaults = UserDefaults.standard - let core = CloudXCore.shared() + let core = CloudXCore.shared switch mutationId { case "gdpr_applies": diff --git a/demo-app-swift/Podfile b/demo-app-swift/Podfile index 24ba459a..88ce4a7e 100644 --- a/demo-app-swift/Podfile +++ b/demo-app-swift/Podfile @@ -11,6 +11,14 @@ target 'CloudXSwiftRemotePods' do pod 'CloudXMintegralAdapter', '~> 3.1.0' pod 'CloudXUnityAdsAdapter', '~> 3.1.0' + # Local QA harness. Enabled only when CLX_QA_HARNESS=1 so normal dev/release + # builds remain unchanged. DeepLinkRouter is guarded by + # canImport(CloudXTestHarness) and falls back to no-op stubs when the harness + # is absent. + if ENV['CLX_QA_HARNESS'] == '1' + pod 'CloudXTestHarness', :path => '../../cloudx-qa-automation/sdk/ios/CloudXTestHarness' + end + target 'CloudXSwiftRemotePodsTests' do inherit! :search_paths end diff --git a/demo-app-swift/Podfile.lock b/demo-app-swift/Podfile.lock index 0407e7de..ecdb3580 100644 --- a/demo-app-swift/Podfile.lock +++ b/demo-app-swift/Podfile.lock @@ -1,24 +1,24 @@ PODS: - - CloudXCore (2.2.9) - - CloudXInMobiAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXCore (3.1.0) + - CloudXInMobiAdapter (3.1.0): + - CloudXCore (= 3.1.0) - InMobiSDK (< 12.0, >= 11.0.0) - - CloudXMetaAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXMetaAdapter (3.1.0): + - CloudXCore (= 3.1.0) - FBAudienceNetwork (< 7.0, >= 6.9.0) - - CloudXMintegralAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXMintegralAdapter (3.1.0): + - CloudXCore (= 3.1.0) - MintegralAdSDK (~> 8.0) - MintegralAdSDK/BidBannerAd (~> 8.0) - MintegralAdSDK/BidNewInterstitialAd (~> 8.0) - MintegralAdSDK/BidRewardVideoAd (~> 8.0) - - CloudXRenderer (2.2.9): - - CloudXCore (= 2.2.9) - - CloudXUnityAdsAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXRenderer (3.1.0): + - CloudXCore (= 3.1.0) + - CloudXUnityAdsAdapter (3.1.0): + - CloudXCore (= 3.1.0) - UnityAds (~> 4.17.0) - - CloudXVungleAdapter (2.2.9): - - CloudXCore (= 2.2.9) + - CloudXVungleAdapter (3.1.0): + - CloudXCore (= 3.1.0) - VungleAds (< 8.0, >= 7.4.0) - FBAudienceNetwork (6.21.1) - InMobiSDK (11.2.0) @@ -58,16 +58,16 @@ PODS: - MintegralAdSDK/RewardVideoAd (8.1.1): - MintegralAdSDK/NativeAd - UnityAds (4.17.0) - - VungleAds (7.7.1) + - VungleAds (7.7.2) DEPENDENCIES: - - CloudXCore (~> 2.2.9) - - CloudXInMobiAdapter (~> 2.2.9) - - CloudXMetaAdapter (~> 2.2.9) - - CloudXMintegralAdapter (~> 2.2.9) - - CloudXRenderer (~> 2.2.9) - - CloudXUnityAdsAdapter (~> 2.2.9) - - CloudXVungleAdapter (~> 2.2.9) + - CloudXCore (~> 3.1.0) + - CloudXInMobiAdapter (~> 3.1.0) + - CloudXMetaAdapter (~> 3.1.0) + - CloudXMintegralAdapter (~> 3.1.0) + - CloudXRenderer (~> 3.1.0) + - CloudXUnityAdsAdapter (~> 3.1.0) + - CloudXVungleAdapter (~> 3.1.0) SPEC REPOS: trunk: @@ -85,19 +85,19 @@ SPEC REPOS: - VungleAds SPEC CHECKSUMS: - CloudXCore: 409c559936a63bb1364cf85237e72129da03ecfc - CloudXInMobiAdapter: 28c1f8cea73c480c3e03fbd0b04a813b3e4f5a1a - CloudXMetaAdapter: c50d3e86b24a6cd82b552cea58a99cec7fa00e4f - CloudXMintegralAdapter: 6f3b5dcf1bdf7d388a72d8caec40bd877d8f3dda - CloudXRenderer: 8e37cd5201c2d410a1f84088c51195a5aa29f11a - CloudXUnityAdsAdapter: 0eadb3278c5e2e0211bd6a86d4bc4a4f442c2e81 - CloudXVungleAdapter: 78e4dd5e3216bb881c02cf8615bbf98e52717447 + CloudXCore: 4ec790b817d818e50b485289b832116f7da4ab25 + CloudXInMobiAdapter: 5f2bbfb2f146dd2466592bdc59334cf8cb9b53a3 + CloudXMetaAdapter: a15e48c9ce02d518db49578145a0a2574820e5fa + CloudXMintegralAdapter: 1c92ec727cfad8ae2a6ba5227dba51aaed2a8052 + CloudXRenderer: 56ad15a24c7cf56280f5c2344d2304330668d857 + CloudXUnityAdsAdapter: 270501625d4560e539e157cf8d56efa514959b80 + CloudXVungleAdapter: d82770f30054897fc6fdc444192c20c3e3b5c9ab FBAudienceNetwork: 85d18a65c9e35bc426bd4664cf3ae2a1172d7b60 InMobiSDK: 07312ef7eff433d72e8a0f06ef7b50685b3e2ca5 MintegralAdSDK: 4325769f93ef336237799b5fb455335985fbc8b7 UnityAds: adbe0d522fd14b8298c5e159164fc43396165843 - VungleAds: c1f0dd0d8531b200b9f6a3f78e292ded75540eae + VungleAds: 03d93126f39d6056055a8b9edca701c6a3391d8b -PODFILE CHECKSUM: 8c25af70cdc4ce65a7bd9c8a4cd821ad266ba67d +PODFILE CHECKSUM: 616dd21bb9a4097591ee5251a4ecfcc91a81041f COCOAPODS: 1.16.2