Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions demo-app-objc/CloudXObjCRemotePods/CLXDeepLinkRouter.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
#import "NativeViewController.h"
#import "DemoAppLogger.h"
#import <CloudXTestHarness/CLXTestHarness.h>
#import <CloudXCore/CloudXCore.h>
#import <CloudXCore/CLXKeyValueState.h>
#import <CloudXCore/CLXManualPrivacyState.h>
#import <CloudXCore/CLXUserDefaultsKeys.h>

static NSTimeInterval const kSDKInitTimeout = 30.0;

Expand Down Expand Up @@ -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<NSString *> *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)
Expand Down
8 changes: 8 additions & 0 deletions demo-app-objc/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CloudXTestHarness/CLXTestHarness.h>) 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
Expand Down
60 changes: 30 additions & 30 deletions demo-app-objc/Podfile.lock
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
97 changes: 97 additions & 0 deletions demo-app-swift/CloudXSwiftRemotePods/DeepLinkRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UIKit

#if canImport(CloudXTestHarness)
import CloudXTestHarness
import CloudXCore

private let kSDKInitTimeout: TimeInterval = 30.0

Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions demo-app-swift/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading