cloudx-react-native 3.10.0 → 3.11.0
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/README.md +1 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/cloudxreactnative/CloudXAdEvents.kt +10 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +40 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +718 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +119 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXPackage.kt +4 -1
- package/android/src/main/res/layout/cloudx_rn_native_media_banner_template.xml +100 -0
- package/android/src/main/res/layout/cloudx_rn_native_medium_template.xml +99 -0
- package/android/src/main/res/layout/cloudx_rn_native_small_template.xml +72 -0
- package/android/src/main/res/layout/cloudx_rn_native_vertical_media_template.xml +117 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +1 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +86 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +1 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +118 -0
- package/cloudx-react-native.podspec +1 -1
- package/ios/CloudX.h +8 -0
- package/ios/CloudX.mm +159 -29
- package/ios/CloudXNativeAdView.h +75 -0
- package/ios/CloudXNativeAdView.mm +979 -0
- package/ios/CloudXNativeAdViewManager.h +12 -0
- package/ios/CloudXNativeAdViewManager.mm +119 -0
- package/lib/commonjs/CloudXAdViewBase.js +8 -97
- package/lib/commonjs/CloudXAdViewBase.js.map +1 -1
- package/lib/commonjs/CloudXNativeAdView.js +331 -0
- package/lib/commonjs/CloudXNativeAdView.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdViewComponents.js +252 -0
- package/lib/commonjs/CloudXNativeAdViewComponents.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdViewProvider.js +117 -0
- package/lib/commonjs/CloudXNativeAdViewProvider.js.map +1 -0
- package/lib/commonjs/extraParameters.js +52 -0
- package/lib/commonjs/extraParameters.js.map +1 -0
- package/lib/commonjs/hooks.js +34 -0
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +72 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +41 -0
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
- package/lib/commonjs/types/NativeAd.js +70 -0
- package/lib/commonjs/types/NativeAd.js.map +1 -0
- package/lib/commonjs/types/index.js +13 -0
- package/lib/commonjs/types/index.js.map +1 -1
- package/lib/commonjs/useCloudXSdkInitialization.js +75 -0
- package/lib/commonjs/useCloudXSdkInitialization.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/module/CloudXAdViewBase.js +7 -96
- package/lib/module/CloudXAdViewBase.js.map +1 -1
- package/lib/module/CloudXNativeAdView.js +324 -0
- package/lib/module/CloudXNativeAdView.js.map +1 -0
- package/lib/module/CloudXNativeAdViewComponents.js +240 -0
- package/lib/module/CloudXNativeAdViewComponents.js.map +1 -0
- package/lib/module/CloudXNativeAdViewProvider.js +110 -0
- package/lib/module/CloudXNativeAdViewProvider.js.map +1 -0
- package/lib/module/extraParameters.js +45 -0
- package/lib/module/extraParameters.js.map +1 -0
- package/lib/module/hooks.js +34 -0
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +35 -0
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
- package/lib/module/types/NativeAd.js +68 -0
- package/lib/module/types/NativeAd.js.map +1 -0
- package/lib/module/types/index.js +1 -0
- package/lib/module/types/index.js.map +1 -1
- package/lib/module/useCloudXSdkInitialization.js +68 -0
- package/lib/module/useCloudXSdkInitialization.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/CloudXAdViewBase.d.ts.map +1 -1
- package/lib/typescript/src/CloudXNativeAdView.d.ts +24 -0
- package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts +67 -0
- package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts +72 -0
- package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts.map +1 -0
- package/lib/typescript/src/extraParameters.d.ts +4 -0
- package/lib/typescript/src/extraParameters.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +103 -0
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types/AdInfo.d.ts +4 -1
- package/lib/typescript/src/types/AdInfo.d.ts.map +1 -1
- package/lib/typescript/src/types/NativeAd.d.ts +114 -0
- package/lib/typescript/src/types/NativeAd.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +2 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/useCloudXSdkInitialization.d.ts +16 -0
- package/lib/typescript/src/useCloudXSdkInitialization.d.ts.map +1 -0
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/CloudXAdViewBase.tsx +8 -125
- package/src/CloudXNativeAdView.tsx +419 -0
- package/src/CloudXNativeAdViewComponents.tsx +203 -0
- package/src/CloudXNativeAdViewProvider.tsx +149 -0
- package/src/extraParameters.ts +65 -0
- package/src/hooks.ts +38 -0
- package/src/index.ts +24 -0
- package/src/specs/CloudXNativeAdViewNativeComponent.ts +137 -0
- package/src/types/AdInfo.ts +5 -1
- package/src/types/NativeAd.ts +125 -0
- package/src/types/index.ts +7 -0
- package/src/useCloudXSdkInitialization.ts +73 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
//
|
|
2
|
+
// CloudXNativeAdView.mm
|
|
3
|
+
// cloudx-react-native
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
#import "CloudXNativeAdView.h"
|
|
7
|
+
#import "CloudX.h"
|
|
8
|
+
#import <React/RCTLog.h>
|
|
9
|
+
// Supplies `UIView.reactTag`, which Paper uses instead of `UIView.tag`.
|
|
10
|
+
#import <React/UIView+React.h>
|
|
11
|
+
|
|
12
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
13
|
+
#import <react/renderer/components/CloudXSpec/ComponentDescriptors.h>
|
|
14
|
+
#import <react/renderer/components/CloudXSpec/EventEmitters.h>
|
|
15
|
+
#import <react/renderer/components/CloudXSpec/Props.h>
|
|
16
|
+
#import <react/renderer/components/CloudXSpec/RCTComponentViewHelpers.h>
|
|
17
|
+
#import <React/RCTConversions.h>
|
|
18
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
19
|
+
|
|
20
|
+
using namespace facebook::react;
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Fills a generated NativeAdInfo-style event struct from the NSDictionary the
|
|
24
|
+
* ad delegate builds. onAdLoaded/onAdClicked/onAdExpired/onAdClosed/
|
|
25
|
+
* onAdRevenuePaid all share this field set (see EventEmitters.h), so this
|
|
26
|
+
* macro keeps the five emitter blocks in sync.
|
|
27
|
+
*/
|
|
28
|
+
#define CLX_FILL_NATIVE_ADINFO(result, event) \
|
|
29
|
+
result.adUnitId = std::string([(event[@"adUnitId"] ?: @"") UTF8String]); \
|
|
30
|
+
result.adFormat = std::string([(event[@"adFormat"] ?: @"") UTF8String]); \
|
|
31
|
+
result.networkName = std::string([(event[@"networkName"] ?: @"") UTF8String]); \
|
|
32
|
+
result.networkPlacement = std::string([(event[@"networkPlacement"] ?: @"") UTF8String]); \
|
|
33
|
+
result.placement = std::string([(event[@"placement"] ?: @"") UTF8String]); \
|
|
34
|
+
result.creativeId = std::string([(event[@"creativeId"] ?: @"") UTF8String]); \
|
|
35
|
+
result.revenue = [event[@"revenue"] doubleValue]; \
|
|
36
|
+
result.adValuesJson = std::string([(event[@"adValuesJson"] ?: @"") UTF8String]); \
|
|
37
|
+
result.nativeAdJson = std::string([(event[@"nativeAdJson"] ?: @"") UTF8String]);
|
|
38
|
+
|
|
39
|
+
static id CLXNativeIdFromFollyDynamic(const folly::dynamic &dynamicValue)
|
|
40
|
+
{
|
|
41
|
+
switch ( dynamicValue.type() )
|
|
42
|
+
{
|
|
43
|
+
case folly::dynamic::NULLT:
|
|
44
|
+
return [NSNull null];
|
|
45
|
+
case folly::dynamic::BOOL:
|
|
46
|
+
return @(dynamicValue.getBool());
|
|
47
|
+
case folly::dynamic::INT64:
|
|
48
|
+
return @(dynamicValue.getInt());
|
|
49
|
+
case folly::dynamic::DOUBLE:
|
|
50
|
+
return @(dynamicValue.getDouble());
|
|
51
|
+
case folly::dynamic::STRING:
|
|
52
|
+
return [[NSString alloc] initWithBytes: dynamicValue.c_str() length: dynamicValue.size() encoding: NSUTF8StringEncoding] ?: @"";
|
|
53
|
+
case folly::dynamic::ARRAY: {
|
|
54
|
+
NSMutableArray *array = [NSMutableArray arrayWithCapacity: dynamicValue.size()];
|
|
55
|
+
for ( const auto &item : dynamicValue )
|
|
56
|
+
{
|
|
57
|
+
[array addObject: CLXNativeIdFromFollyDynamic(item) ?: [NSNull null]];
|
|
58
|
+
}
|
|
59
|
+
return [array copy];
|
|
60
|
+
}
|
|
61
|
+
case folly::dynamic::OBJECT: {
|
|
62
|
+
NSMutableDictionary<NSString *, id> *dictionary = [NSMutableDictionary dictionaryWithCapacity: dynamicValue.size()];
|
|
63
|
+
for ( const auto &item : dynamicValue.items() )
|
|
64
|
+
{
|
|
65
|
+
id key = CLXNativeIdFromFollyDynamic(item.first);
|
|
66
|
+
if ( ![key isKindOfClass: [NSString class]] ) continue;
|
|
67
|
+
dictionary[key] = CLXNativeIdFromFollyDynamic(item.second) ?: [NSNull null];
|
|
68
|
+
}
|
|
69
|
+
return [dictionary copy];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [NSNull null];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// CloudXNativeAdViewProps declares extraParameters and localExtraParameters as
|
|
77
|
+
// two distinct generated struct types with an identical {key, value} shape;
|
|
78
|
+
// share one conversion instead of writing it out twice.
|
|
79
|
+
template <typename StructT>
|
|
80
|
+
static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStructs(const std::vector<StructT> &entries)
|
|
81
|
+
{
|
|
82
|
+
NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray arrayWithCapacity: entries.size()];
|
|
83
|
+
for ( const auto &entry : entries )
|
|
84
|
+
{
|
|
85
|
+
NSString *key = [NSString stringWithUTF8String: entry.key.c_str()] ?: @"";
|
|
86
|
+
id value = CLXNativeIdFromFollyDynamic(entry.value) ?: [NSNull null];
|
|
87
|
+
[result addObject: @{@"key": key, @"value": value}];
|
|
88
|
+
}
|
|
89
|
+
return [result copy];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@interface CloudXNativeAdView () <RCTCloudXNativeAdViewViewProtocol>
|
|
93
|
+
#else
|
|
94
|
+
@interface CloudXNativeAdView ()
|
|
95
|
+
#endif
|
|
96
|
+
|
|
97
|
+
@property (nonatomic, copy) NSArray<NSDictionary<NSString *, id> *> *extraParameterEntries;
|
|
98
|
+
@property (nonatomic, copy) NSArray<NSDictionary<NSString *, id> *> *localExtraParameterEntries;
|
|
99
|
+
|
|
100
|
+
/// Per-host loader. Created lazily when @c adUnitId is known so each mounted
|
|
101
|
+
/// component has its own load pipeline and delegate routing — sharing one
|
|
102
|
+
/// loader across concurrently-mounted hosts for the same ad unit would cause
|
|
103
|
+
/// delegate reassignment (callbacks delivered to whichever host attached
|
|
104
|
+
/// most recently) and duplicate-load rejection (hosts beyond the first never
|
|
105
|
+
/// load).
|
|
106
|
+
@property (nonatomic, strong, nullable) CLXNativeAdLoader *nativeAdLoader;
|
|
107
|
+
|
|
108
|
+
/// Bumped whenever in-flight work for this host becomes invalid — a destroyAd
|
|
109
|
+
/// or a live re-target. Delegate callbacks read it at entry, before their hop
|
|
110
|
+
/// to the main queue, and drop themselves if it has moved since. Without this
|
|
111
|
+
/// an auction already in flight when the host was destroyed still rendered a
|
|
112
|
+
/// view and fired onAdLoaded. Atomic because callbacks may arrive on any
|
|
113
|
+
/// thread while the main thread advances it.
|
|
114
|
+
@property (atomic, assign) NSUInteger loadGeneration;
|
|
115
|
+
|
|
116
|
+
/// Currently-rendered template view. Nil when the JS layer is driving a
|
|
117
|
+
/// manual render.
|
|
118
|
+
@property (nonatomic, strong, nullable) CLXNativeAdView *currentNativeView;
|
|
119
|
+
|
|
120
|
+
/// The CLXAd backing @c currentNativeView . Tracked separately so a template
|
|
121
|
+
/// reload can destroy the ad being replaced even when @c pendingManualAd has
|
|
122
|
+
/// already been overwritten. Nil in manual mode.
|
|
123
|
+
@property (nonatomic, strong, nullable) CLXAd *currentTemplateAd;
|
|
124
|
+
|
|
125
|
+
/// The most recently loaded ad. Held so @c renderNativeAd can wire up click
|
|
126
|
+
/// tracking after the JS layer finishes binding asset views.
|
|
127
|
+
@property (nonatomic, strong, nullable) CLXAd *pendingManualAd;
|
|
128
|
+
|
|
129
|
+
/// Publisher-supplied clickable subviews, collected by @c updateAssetView .
|
|
130
|
+
@property (nonatomic, strong) NSMutableArray<UIView *> *clickableViews;
|
|
131
|
+
/// Tap path for container-clickable adapters in manual mode. See
|
|
132
|
+
/// -clx_updateContainerClickRecognizerForAd:.
|
|
133
|
+
@property (nonatomic, strong, nullable) UITapGestureRecognizer *containerClickRecognizer;
|
|
134
|
+
|
|
135
|
+
/// Maps asset role name to the publisher-supplied UIView. Used so a re-bind
|
|
136
|
+
/// of the same role replaces the prior entry without leaving stale
|
|
137
|
+
/// references in @c clickableViews .
|
|
138
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, UIView *> *assetViewsByRole;
|
|
139
|
+
|
|
140
|
+
/// SDK-inserted media/options subviews added into the publisher's asset
|
|
141
|
+
/// containers during manual binding. Tracked so a reload or destroy can pull
|
|
142
|
+
/// them back out; without this, successive reloads stack multiple
|
|
143
|
+
/// mediaView/optionsView instances inside the same container.
|
|
144
|
+
@property (nonatomic, strong) NSMutableArray<UIView *> *insertedSDKSubviews;
|
|
145
|
+
|
|
146
|
+
@property (nonatomic, assign) BOOL hasRequestedLoad;
|
|
147
|
+
|
|
148
|
+
@end
|
|
149
|
+
|
|
150
|
+
@implementation CloudXNativeAdView
|
|
151
|
+
|
|
152
|
+
- (void)commonInit
|
|
153
|
+
{
|
|
154
|
+
_extraParameterEntries = @[];
|
|
155
|
+
_localExtraParameterEntries = @[];
|
|
156
|
+
_clickableViews = [NSMutableArray array];
|
|
157
|
+
_assetViewsByRole = [NSMutableDictionary dictionary];
|
|
158
|
+
_insertedSDKSubviews = [NSMutableArray array];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
162
|
+
|
|
163
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
164
|
+
{
|
|
165
|
+
return concreteComponentDescriptorProvider<CloudXNativeAdViewComponentDescriptor>();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
169
|
+
{
|
|
170
|
+
self = [super initWithFrame: frame];
|
|
171
|
+
if ( self )
|
|
172
|
+
{
|
|
173
|
+
[self commonInit];
|
|
174
|
+
static const auto defaultProps = std::make_shared<const CloudXNativeAdViewProps>();
|
|
175
|
+
_props = defaultProps;
|
|
176
|
+
[self setupEventHandlers];
|
|
177
|
+
}
|
|
178
|
+
return self;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* On Fabric the RCTDirectEventBlock properties are not wired by a view manager,
|
|
183
|
+
* so point each block at the generated C++ EventEmitter. The delegate callbacks
|
|
184
|
+
* below always call self.onXxxEvent(dict) regardless of architecture.
|
|
185
|
+
*/
|
|
186
|
+
- (void)setupEventHandlers
|
|
187
|
+
{
|
|
188
|
+
__weak CloudXNativeAdView *weakSelf = self;
|
|
189
|
+
|
|
190
|
+
self.onAdLoadedEvent = ^(NSDictionary *event) {
|
|
191
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
192
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
193
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
194
|
+
CloudXNativeAdViewEventEmitter::OnAdLoadedEvent result;
|
|
195
|
+
CLX_FILL_NATIVE_ADINFO(result, event);
|
|
196
|
+
emitter->onAdLoadedEvent(result);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
self.onAdClickedEvent = ^(NSDictionary *event) {
|
|
200
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
201
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
202
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
203
|
+
CloudXNativeAdViewEventEmitter::OnAdClickedEvent result;
|
|
204
|
+
CLX_FILL_NATIVE_ADINFO(result, event);
|
|
205
|
+
emitter->onAdClickedEvent(result);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
self.onAdExpiredEvent = ^(NSDictionary *event) {
|
|
209
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
210
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
211
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
212
|
+
CloudXNativeAdViewEventEmitter::OnAdExpiredEvent result;
|
|
213
|
+
CLX_FILL_NATIVE_ADINFO(result, event);
|
|
214
|
+
emitter->onAdExpiredEvent(result);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
self.onAdClosedEvent = ^(NSDictionary *event) {
|
|
218
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
219
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
220
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
221
|
+
CloudXNativeAdViewEventEmitter::OnAdClosedEvent result;
|
|
222
|
+
CLX_FILL_NATIVE_ADINFO(result, event);
|
|
223
|
+
emitter->onAdClosedEvent(result);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
self.onAdRevenuePaidEvent = ^(NSDictionary *event) {
|
|
227
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
228
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
229
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
230
|
+
CloudXNativeAdViewEventEmitter::OnAdRevenuePaidEvent result;
|
|
231
|
+
CLX_FILL_NATIVE_ADINFO(result, event);
|
|
232
|
+
emitter->onAdRevenuePaidEvent(result);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
self.onAdLoadFailedEvent = ^(NSDictionary *event) {
|
|
236
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
237
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
238
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
239
|
+
CloudXNativeAdViewEventEmitter::OnAdLoadFailedEvent result;
|
|
240
|
+
result.adUnitId = std::string([(event[@"adUnitId"] ?: @"") UTF8String]);
|
|
241
|
+
result.code = [event[@"code"] doubleValue];
|
|
242
|
+
result.message = std::string([(event[@"message"] ?: @"") UTF8String]);
|
|
243
|
+
emitter->onAdLoadFailedEvent(result);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
self.onAdDisplayFailedEvent = ^(NSDictionary *event) {
|
|
247
|
+
CloudXNativeAdView *strongSelf = weakSelf;
|
|
248
|
+
if ( !strongSelf || !strongSelf->_eventEmitter ) return;
|
|
249
|
+
auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
|
|
250
|
+
CloudXNativeAdViewEventEmitter::OnAdDisplayFailedEvent result;
|
|
251
|
+
result.adUnitId = std::string([(event[@"adUnitId"] ?: @"") UTF8String]);
|
|
252
|
+
result.code = [event[@"code"] doubleValue];
|
|
253
|
+
result.message = std::string([(event[@"message"] ?: @"") UTF8String]);
|
|
254
|
+
emitter->onAdDisplayFailedEvent(result);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
259
|
+
{
|
|
260
|
+
const auto &oldViewProps = *std::static_pointer_cast<CloudXNativeAdViewProps const>(_props);
|
|
261
|
+
const auto &newViewProps = *std::static_pointer_cast<CloudXNativeAdViewProps const>(props);
|
|
262
|
+
|
|
263
|
+
if ( oldViewProps.adUnitId != newViewProps.adUnitId )
|
|
264
|
+
{
|
|
265
|
+
self.adUnitId = RCTNSStringFromString(newViewProps.adUnitId);
|
|
266
|
+
}
|
|
267
|
+
if ( oldViewProps.templateName != newViewProps.templateName )
|
|
268
|
+
{
|
|
269
|
+
self.templateName = RCTNSStringFromStringNilIfEmpty(newViewProps.templateName);
|
|
270
|
+
}
|
|
271
|
+
if ( oldViewProps.placement != newViewProps.placement )
|
|
272
|
+
{
|
|
273
|
+
self.placement = RCTNSStringFromStringNilIfEmpty(newViewProps.placement);
|
|
274
|
+
}
|
|
275
|
+
if ( oldViewProps.customData != newViewProps.customData )
|
|
276
|
+
{
|
|
277
|
+
self.customData = RCTNSStringFromStringNilIfEmpty(newViewProps.customData);
|
|
278
|
+
}
|
|
279
|
+
if ( oldViewProps.disableVideoFullScreen != newViewProps.disableVideoFullScreen )
|
|
280
|
+
{
|
|
281
|
+
self.disableVideoFullScreen = newViewProps.disableVideoFullScreen;
|
|
282
|
+
}
|
|
283
|
+
if ( oldViewProps.startVideoUnmuted != newViewProps.startVideoUnmuted )
|
|
284
|
+
{
|
|
285
|
+
self.startVideoUnmuted = newViewProps.startVideoUnmuted;
|
|
286
|
+
}
|
|
287
|
+
if ( oldViewProps.hideVideoMediaControls != newViewProps.hideVideoMediaControls )
|
|
288
|
+
{
|
|
289
|
+
self.hideVideoMediaControls = newViewProps.hideVideoMediaControls;
|
|
290
|
+
}
|
|
291
|
+
if ( oldViewProps.manualRender != newViewProps.manualRender )
|
|
292
|
+
{
|
|
293
|
+
self.manualRender = newViewProps.manualRender;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* CloudXNativeAdViewExtraParametersStruct/LocalExtraParametersStruct
|
|
298
|
+
* contain folly::dynamic, so the generated props do not support a simple
|
|
299
|
+
* old/new equality guard like the scalar props above (see CloudXAdView's
|
|
300
|
+
* identical rationale). Applying is idempotent — values are only read
|
|
301
|
+
* again at the next loadAd.
|
|
302
|
+
*/
|
|
303
|
+
[self setExtraParametersFromEntryArray: CLXEntryArrayFromNativeAdViewStructs(newViewProps.extraParameters)];
|
|
304
|
+
[self setLocalExtraParametersFromEntryArray: CLXEntryArrayFromNativeAdViewStructs(newViewProps.localExtraParameters)];
|
|
305
|
+
|
|
306
|
+
[super updateProps: props oldProps: oldProps];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
310
|
+
{
|
|
311
|
+
RCTCloudXNativeAdViewHandleCommand(self, commandName, args);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
- (void)prepareForRecycle
|
|
315
|
+
{
|
|
316
|
+
[super prepareForRecycle];
|
|
317
|
+
|
|
318
|
+
static const auto defaultProps = std::make_shared<const CloudXNativeAdViewProps>();
|
|
319
|
+
_props = defaultProps;
|
|
320
|
+
|
|
321
|
+
// Unlike CloudXAdView (whose SDK-owned ad view is created and destroyed
|
|
322
|
+
// together with each mount), this host's CLXNativeAdLoader is a
|
|
323
|
+
// persistent, stateful per-instance object. A recycled Fabric view must
|
|
324
|
+
// never leak the prior mount's ad/loader state into the next mount.
|
|
325
|
+
// destroyAd detaches the delegates, destroys the loader and nils it.
|
|
326
|
+
[self destroyAd];
|
|
327
|
+
|
|
328
|
+
self.adUnitId = nil;
|
|
329
|
+
self.templateName = nil;
|
|
330
|
+
self.placement = nil;
|
|
331
|
+
self.customData = nil;
|
|
332
|
+
self.disableVideoFullScreen = NO;
|
|
333
|
+
self.startVideoUnmuted = NO;
|
|
334
|
+
self.hideVideoMediaControls = NO;
|
|
335
|
+
self.manualRender = NO;
|
|
336
|
+
self.extraParameterEntries = @[];
|
|
337
|
+
self.localExtraParameterEntries = @[];
|
|
338
|
+
self.hasRequestedLoad = NO;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#else
|
|
342
|
+
|
|
343
|
+
- (instancetype)init
|
|
344
|
+
{
|
|
345
|
+
self = [super init];
|
|
346
|
+
if ( self )
|
|
347
|
+
{
|
|
348
|
+
[self commonInit];
|
|
349
|
+
}
|
|
350
|
+
return self;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#endif // RCT_NEW_ARCH_ENABLED
|
|
354
|
+
|
|
355
|
+
#pragma mark - Property setters (shared; Paper view manager sets these via KVC)
|
|
356
|
+
|
|
357
|
+
- (void)setAdUnitId:(NSString *)adUnitId
|
|
358
|
+
{
|
|
359
|
+
NSString *normalized = [adUnitId copy];
|
|
360
|
+
if ( [normalized isEqual: _adUnitId] ) return;
|
|
361
|
+
|
|
362
|
+
// First-time set (no loader yet): just store the value. didMoveToWindow /
|
|
363
|
+
// loadIfNeeded loads once the view is attached.
|
|
364
|
+
if ( !self.nativeAdLoader )
|
|
365
|
+
{
|
|
366
|
+
_adUnitId = normalized;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Live re-target — parity with Android's setAdUnitId. destroyAd detaches
|
|
371
|
+
// and destroys the loader bound to the previous ad unit, so its in-flight
|
|
372
|
+
// auction cannot keep rendering into this host, and it clears
|
|
373
|
+
// hasRequestedLoad so the new unit is allowed to load.
|
|
374
|
+
[self destroyAd];
|
|
375
|
+
|
|
376
|
+
_adUnitId = normalized;
|
|
377
|
+
|
|
378
|
+
if ( !self.window ) return;
|
|
379
|
+
|
|
380
|
+
// The sibling props — placement, customData, the video flags and the extra
|
|
381
|
+
// parameters — are applied AFTER adUnitId in the same update pass (Fabric
|
|
382
|
+
// updateProps: in declaration order; Paper via KVC in the view manager's
|
|
383
|
+
// order). Loading synchronously here therefore configured the loader from
|
|
384
|
+
// the PREVIOUS unit's values. Deferring to the end of this main-thread
|
|
385
|
+
// turn makes the load independent of prop ordering on both architectures.
|
|
386
|
+
NSUInteger generation = self.loadGeneration;
|
|
387
|
+
__weak __typeof(self) weakSelf = self;
|
|
388
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
389
|
+
__typeof(self) strongSelf = weakSelf;
|
|
390
|
+
if ( !strongSelf || generation != strongSelf.loadGeneration ) return;
|
|
391
|
+
[strongSelf loadIfNeeded];
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
- (void)setExtraParametersFromEntryArray:(nullable NSArray<NSDictionary<NSString *, id> *> *)extraParameters
|
|
396
|
+
{
|
|
397
|
+
self.extraParameterEntries = extraParameters ?: @[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
- (void)setLocalExtraParametersFromEntryArray:(nullable NSArray<NSDictionary<NSString *, id> *> *)localExtraParameters
|
|
401
|
+
{
|
|
402
|
+
self.localExtraParameterEntries = localExtraParameters ?: @[];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#pragma mark - Ad view lifecycle
|
|
406
|
+
|
|
407
|
+
- (void)didMoveToWindow
|
|
408
|
+
{
|
|
409
|
+
[super didMoveToWindow];
|
|
410
|
+
// RN sets all view properties before the view is inserted into the
|
|
411
|
+
// hierarchy, so manualRender is settled by the time this callback fires.
|
|
412
|
+
// Auto-load runs in both render modes; the mode only controls how
|
|
413
|
+
// didLoadNativeAd:forAd: presents the result.
|
|
414
|
+
if ( self.window ) [self loadIfNeeded];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
- (CLXNativeAdLoader *)resolveLoader
|
|
418
|
+
{
|
|
419
|
+
if ( self.adUnitId.length == 0 ) return nil;
|
|
420
|
+
|
|
421
|
+
if ( !self.nativeAdLoader )
|
|
422
|
+
{
|
|
423
|
+
// A dedicated loader per host so concurrent mounts for the same ad
|
|
424
|
+
// unit do not collide on delegate registration or loadAd gating.
|
|
425
|
+
self.nativeAdLoader = [[CloudXCore shared] createNativeAdLoaderWithAdUnitIdentifier: self.adUnitId];
|
|
426
|
+
self.nativeAdLoader.nativeAdDelegate = self;
|
|
427
|
+
self.nativeAdLoader.revenueDelegate = self;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return self.nativeAdLoader;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
- (void)loadIfNeeded
|
|
434
|
+
{
|
|
435
|
+
if ( self.hasRequestedLoad || self.adUnitId.length == 0 ) return;
|
|
436
|
+
|
|
437
|
+
CLXNativeAdLoader *loader = [self resolveLoader];
|
|
438
|
+
if ( !loader ) return;
|
|
439
|
+
|
|
440
|
+
loader.placement = self.placement;
|
|
441
|
+
loader.customData = self.customData;
|
|
442
|
+
loader.disableVideoFullScreen = self.disableVideoFullScreen;
|
|
443
|
+
loader.startVideoUnmuted = self.startVideoUnmuted;
|
|
444
|
+
loader.hideVideoMediaControls = self.hideVideoMediaControls;
|
|
445
|
+
|
|
446
|
+
[self applyExtraParametersToLoader: loader];
|
|
447
|
+
|
|
448
|
+
self.hasRequestedLoad = YES;
|
|
449
|
+
[loader loadAd];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
- (void)applyExtraParametersToLoader:(CLXNativeAdLoader *)loader
|
|
453
|
+
{
|
|
454
|
+
// JS null crosses the bridge as NSNull; normalize to nil so the loader
|
|
455
|
+
// never receives NSNull.
|
|
456
|
+
//
|
|
457
|
+
// extraParameters are bid-level auction signals (setExtraParameter:value:);
|
|
458
|
+
// localExtraParameters are adapter-only and never sent in the bid request
|
|
459
|
+
// (setLocalExtraParameterForKey:value:) — see CLXNativeAdLoader.h.
|
|
460
|
+
for ( NSDictionary *entry in self.extraParameterEntries )
|
|
461
|
+
{
|
|
462
|
+
NSString *key = [entry[@"key"] isKindOfClass: [NSString class]] ? entry[@"key"] : nil;
|
|
463
|
+
if ( key.length == 0 ) continue;
|
|
464
|
+
id value = entry[@"value"];
|
|
465
|
+
if ( value == [NSNull null] ) value = nil;
|
|
466
|
+
[loader setExtraParameter: key value: value];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for ( NSDictionary *entry in self.localExtraParameterEntries )
|
|
470
|
+
{
|
|
471
|
+
NSString *key = [entry[@"key"] isKindOfClass: [NSString class]] ? entry[@"key"] : nil;
|
|
472
|
+
if ( key.length == 0 ) continue;
|
|
473
|
+
id value = entry[@"value"];
|
|
474
|
+
if ( value == [NSNull null] ) value = nil;
|
|
475
|
+
[loader setLocalExtraParameterForKey: key value: value];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#pragma mark - Commands
|
|
480
|
+
//
|
|
481
|
+
// Method names match RCTCloudXNativeAdViewViewProtocol exactly, so Fabric's
|
|
482
|
+
// generated command dispatch calls these directly (see handleCommand:args:
|
|
483
|
+
// above) and the Old Architecture manager's UIManager-block handlers
|
|
484
|
+
// (CloudXNativeAdViewManager.mm) call the identical selectors.
|
|
485
|
+
|
|
486
|
+
- (void)loadAd
|
|
487
|
+
{
|
|
488
|
+
// Explicit JS-driven (re)load bypasses the "already requested" guard used
|
|
489
|
+
// by the didMoveToWindow auto-load.
|
|
490
|
+
self.hasRequestedLoad = NO;
|
|
491
|
+
[self loadIfNeeded];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/// Depth-first search for a descendant carrying `reactTag`.
|
|
495
|
+
///
|
|
496
|
+
/// The two renderers store the React tag differently: Fabric mirrors it into
|
|
497
|
+
/// `UIView.tag`, while Paper keeps it only in the `reactTag` associated object.
|
|
498
|
+
/// Checking both is what makes asset binding work on either architecture.
|
|
499
|
+
static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
|
|
500
|
+
{
|
|
501
|
+
if ( root.tag == reactTag ) return root;
|
|
502
|
+
NSNumber *paperTag = root.reactTag;
|
|
503
|
+
if ( paperTag != nil && paperTag.integerValue == reactTag ) return root;
|
|
504
|
+
|
|
505
|
+
for ( UIView *subview in root.subviews )
|
|
506
|
+
{
|
|
507
|
+
UIView *found = clx_findDescendantWithReactTag(subview, reactTag);
|
|
508
|
+
if ( found ) return found;
|
|
509
|
+
}
|
|
510
|
+
return nil;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
- (nullable UIView *)clx_descendantWithReactTag:(NSInteger)reactTag
|
|
514
|
+
{
|
|
515
|
+
if ( reactTag == 0 ) return nil;
|
|
516
|
+
return clx_findDescendantWithReactTag(self, reactTag);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
- (void)updateAssetView:(NSInteger)assetViewTag assetName:(NSString *)assetName
|
|
520
|
+
{
|
|
521
|
+
if ( assetName.length == 0 ) return;
|
|
522
|
+
|
|
523
|
+
// Post-destroy / pre-load calls must be observable no-ops: without an ad
|
|
524
|
+
// there is no nativeAd to populate from, and dereferencing it would
|
|
525
|
+
// crash or silently bind against a stale ad.
|
|
526
|
+
if ( !self.pendingManualAd )
|
|
527
|
+
{
|
|
528
|
+
[[CloudX shared] log: @"updateAssetView ignored — no loaded ad (post-destroy or pre-load). role=%@", assetName];
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Descendant lookup, not RCTUIManager/bridge-based resolution: Fabric has
|
|
533
|
+
// no bridge.uiManager, so this must work by walking this host's own view
|
|
534
|
+
// hierarchy on both architectures. `viewWithTag:` alone is not enough —
|
|
535
|
+
// Paper keeps the React tag in the `reactTag` associated object and never
|
|
536
|
+
// in `UIView.tag`, so it resolves nothing there.
|
|
537
|
+
UIView *view = [self clx_descendantWithReactTag: assetViewTag];
|
|
538
|
+
if ( !view ) return;
|
|
539
|
+
|
|
540
|
+
// If a prior binding for this role exists, drop the old reference from
|
|
541
|
+
// clickableViews so only the current view is registered for clicks.
|
|
542
|
+
UIView *prior = self.assetViewsByRole[assetName];
|
|
543
|
+
if ( prior ) [self.clickableViews removeObjectIdenticalTo: prior];
|
|
544
|
+
|
|
545
|
+
self.assetViewsByRole[assetName] = view;
|
|
546
|
+
|
|
547
|
+
CLXNativeAd *nativeAd = self.pendingManualAd.nativeAd;
|
|
548
|
+
|
|
549
|
+
// Deliberately no `view.tag = CLXNativeAdViewTag*` here. Those constants are
|
|
550
|
+
// only read off a CLXNativeAdView container, which manual mode never uses,
|
|
551
|
+
// so stamping them bought nothing — and it overwrote the React tag, so a
|
|
552
|
+
// re-bind of the same mounted layout resolved nothing and the next ad got
|
|
553
|
+
// no media, no options view and an empty clickable list.
|
|
554
|
+
if ( [assetName isEqualToString: @"TitleView"] )
|
|
555
|
+
{
|
|
556
|
+
if ( nativeAd.title.length > 0 ) [self.clickableViews addObject: view];
|
|
557
|
+
}
|
|
558
|
+
else if ( [assetName isEqualToString: @"AdvertiserView"] )
|
|
559
|
+
{
|
|
560
|
+
if ( nativeAd.advertiser.length > 0 ) [self.clickableViews addObject: view];
|
|
561
|
+
}
|
|
562
|
+
else if ( [assetName isEqualToString: @"BodyView"] )
|
|
563
|
+
{
|
|
564
|
+
if ( nativeAd.body.length > 0 ) [self.clickableViews addObject: view];
|
|
565
|
+
}
|
|
566
|
+
else if ( [assetName isEqualToString: @"CallToActionView"] )
|
|
567
|
+
{
|
|
568
|
+
if ( nativeAd.callToAction.length > 0 ) [self.clickableViews addObject: view];
|
|
569
|
+
}
|
|
570
|
+
else if ( [assetName isEqualToString: @"IconView"] )
|
|
571
|
+
{
|
|
572
|
+
[self.clickableViews addObject: view];
|
|
573
|
+
}
|
|
574
|
+
else if ( [assetName isEqualToString: @"OptionsView"] )
|
|
575
|
+
{
|
|
576
|
+
// Adapter-provided options view is hosted as a subview of the
|
|
577
|
+
// publisher's container so click-through + ad-choices behavior
|
|
578
|
+
// stays under the SDK's control.
|
|
579
|
+
if ( nativeAd.optionsView )
|
|
580
|
+
{
|
|
581
|
+
nativeAd.optionsView.frame = view.bounds;
|
|
582
|
+
nativeAd.optionsView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
583
|
+
[view addSubview: nativeAd.optionsView];
|
|
584
|
+
[self.insertedSDKSubviews addObject: nativeAd.optionsView];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if ( [assetName isEqualToString: @"MediaView"] )
|
|
588
|
+
{
|
|
589
|
+
if ( nativeAd.mediaView )
|
|
590
|
+
{
|
|
591
|
+
nativeAd.mediaView.frame = view.bounds;
|
|
592
|
+
nativeAd.mediaView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
593
|
+
[view addSubview: nativeAd.mediaView];
|
|
594
|
+
[self.insertedSDKSubviews addObject: nativeAd.mediaView];
|
|
595
|
+
}
|
|
596
|
+
[self.clickableViews addObject: view];
|
|
597
|
+
}
|
|
598
|
+
else if ( [assetName isEqualToString: @"StarRatingView"] )
|
|
599
|
+
{
|
|
600
|
+
// Not clickable; the rating is decorative.
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
- (void)renderNativeAd
|
|
605
|
+
{
|
|
606
|
+
CLXAd *ad = self.pendingManualAd;
|
|
607
|
+
if ( !ad || !ad.nativeAd )
|
|
608
|
+
{
|
|
609
|
+
CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterInternalError
|
|
610
|
+
description: @"renderNativeAd called before ad loaded"];
|
|
611
|
+
if ( self.onAdDisplayFailedEvent ) self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: self.adUnitId ?: @"" error: error]);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if ( ad.nativeAd.isExpired )
|
|
615
|
+
{
|
|
616
|
+
CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterAdExpired
|
|
617
|
+
description: @"renderNativeAd: ad expired"];
|
|
618
|
+
if ( self.onAdDisplayFailedEvent ) self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: ad.adUnitId ?: @"" error: error]);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Publisher drove the render — a lingering template view from a prior
|
|
623
|
+
// load would sit beneath their subviews. Remove it so only the manual
|
|
624
|
+
// hierarchy is visible.
|
|
625
|
+
if ( self.currentNativeView )
|
|
626
|
+
{
|
|
627
|
+
[self.currentNativeView removeFromSuperview];
|
|
628
|
+
self.currentNativeView = nil;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
NSArray<UIView *> *clickables = [self.clickableViews copy];
|
|
632
|
+
UIView *container = self;
|
|
633
|
+
|
|
634
|
+
void (^prepareBlock)(void) = ^{
|
|
635
|
+
BOOL registered = [ad.nativeAd prepareForInteractionClickableViews: clickables withContainer: container];
|
|
636
|
+
if ( registered )
|
|
637
|
+
{
|
|
638
|
+
[self clx_updateContainerClickRecognizerForAd: ad];
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// A load that cannot register click/impression tracking is a display
|
|
643
|
+
// failure: the ad renders but never produces revenue. onAdLoadedEvent
|
|
644
|
+
// has already fired by the time renderNativeAd is invoked, so
|
|
645
|
+
// surface the failure here to make it observable (mirrors Android).
|
|
646
|
+
[[CloudX shared] log: @"Native ad failed to register click/impression tracking — emitting onAdDisplayFailedEvent. adUnitId=%@", ad.adUnitId ?: @""];
|
|
647
|
+
CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterInternalError
|
|
648
|
+
description: @"Native ad failed to register click/impression tracking"];
|
|
649
|
+
if ( self.onAdDisplayFailedEvent ) self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: ad.adUnitId ?: @"" error: error]);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// Interaction registration touches UIKit; always marshal to the main queue.
|
|
653
|
+
if ( ![NSThread isMainThread] )
|
|
654
|
+
{
|
|
655
|
+
dispatch_async(dispatch_get_main_queue(), prepareBlock);
|
|
656
|
+
}
|
|
657
|
+
else
|
|
658
|
+
{
|
|
659
|
+
prepareBlock();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/// Installs or removes the container tap path for the current ad.
|
|
664
|
+
///
|
|
665
|
+
/// Adapters that report `isContainerClickable` do not use the per-asset
|
|
666
|
+
/// clickable views: they expect a tap anywhere on the container to be routed
|
|
667
|
+
/// through `performClick`. The SDK's own native ad view installs that gesture
|
|
668
|
+
/// when it renders, but manual mode registers the publisher's container
|
|
669
|
+
/// instead and never goes through that path — so those ads rendered and could
|
|
670
|
+
/// never register a click. Installing it here restores the click path without
|
|
671
|
+
/// disturbing adapters that track the asset views themselves.
|
|
672
|
+
- (void)clx_updateContainerClickRecognizerForAd:(CLXAd *)ad
|
|
673
|
+
{
|
|
674
|
+
if ( self.containerClickRecognizer )
|
|
675
|
+
{
|
|
676
|
+
[self removeGestureRecognizer: self.containerClickRecognizer];
|
|
677
|
+
self.containerClickRecognizer = nil;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if ( ![ad.nativeAd isContainerClickable] ) return;
|
|
681
|
+
|
|
682
|
+
UITapGestureRecognizer *tap =
|
|
683
|
+
[[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(clx_handleContainerTap:)];
|
|
684
|
+
// The publisher's own controls stay interactive; this only observes taps.
|
|
685
|
+
tap.cancelsTouchesInView = NO;
|
|
686
|
+
self.containerClickRecognizer = tap;
|
|
687
|
+
[self addGestureRecognizer: tap];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
- (void)clx_handleContainerTap:(UITapGestureRecognizer *)recognizer
|
|
691
|
+
{
|
|
692
|
+
CLXNativeAd *nativeAd = self.pendingManualAd.nativeAd;
|
|
693
|
+
if ( !nativeAd || nativeAd.isExpired ) return;
|
|
694
|
+
[nativeAd performClick];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
- (void)destroyAd
|
|
698
|
+
{
|
|
699
|
+
// Invalidate first: a load already in flight belongs to the generation
|
|
700
|
+
// being torn down, and its callback must not render into this host.
|
|
701
|
+
self.loadGeneration++;
|
|
702
|
+
|
|
703
|
+
CLXAd *pendingAd = self.pendingManualAd;
|
|
704
|
+
CLXAd *templateAd = self.currentTemplateAd;
|
|
705
|
+
|
|
706
|
+
// Deliberately NOT resolveLoader — that lazily creates a loader, so a
|
|
707
|
+
// destroy with nothing loaded used to construct the very pipeline it was
|
|
708
|
+
// asked to tear down.
|
|
709
|
+
CLXNativeAdLoader *loader = self.nativeAdLoader;
|
|
710
|
+
if ( loader )
|
|
711
|
+
{
|
|
712
|
+
// Detach before destroying so the SDK cannot deliver a pending load
|
|
713
|
+
// into a host that has already been cleared.
|
|
714
|
+
loader.nativeAdDelegate = nil;
|
|
715
|
+
loader.revenueDelegate = nil;
|
|
716
|
+
|
|
717
|
+
if ( pendingAd ) [loader destroyAd: pendingAd];
|
|
718
|
+
if ( templateAd && templateAd != pendingAd ) [loader destroyAd: templateAd];
|
|
719
|
+
[loader destroy];
|
|
720
|
+
|
|
721
|
+
self.nativeAdLoader = nil;
|
|
722
|
+
}
|
|
723
|
+
self.pendingManualAd = nil;
|
|
724
|
+
self.currentTemplateAd = nil;
|
|
725
|
+
|
|
726
|
+
[self.currentNativeView removeFromSuperview];
|
|
727
|
+
self.currentNativeView = nil;
|
|
728
|
+
|
|
729
|
+
[self removeSDKInsertedSubviews];
|
|
730
|
+
[self.clickableViews removeAllObjects];
|
|
731
|
+
[self.assetViewsByRole removeAllObjects];
|
|
732
|
+
|
|
733
|
+
// The container tap path belongs to the ad being torn down; leaving it
|
|
734
|
+
// attached would route taps to a destroyed ad.
|
|
735
|
+
if ( self.containerClickRecognizer )
|
|
736
|
+
{
|
|
737
|
+
[self removeGestureRecognizer: self.containerClickRecognizer];
|
|
738
|
+
self.containerClickRecognizer = nil;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
self.hasRequestedLoad = NO;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
- (void)removeSDKInsertedSubviews
|
|
745
|
+
{
|
|
746
|
+
for ( UIView *subview in self.insertedSDKSubviews )
|
|
747
|
+
{
|
|
748
|
+
[subview removeFromSuperview];
|
|
749
|
+
}
|
|
750
|
+
[self.insertedSDKSubviews removeAllObjects];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
- (void)dealloc
|
|
754
|
+
{
|
|
755
|
+
// The loader is owned by this host. Destroying it releases the SDK-side
|
|
756
|
+
// ad pipeline and cancels any in-flight auction so nothing calls back
|
|
757
|
+
// into the zombie delegate. Keep strong local references across the
|
|
758
|
+
// async hop — the block intentionally does not capture self. Capture the
|
|
759
|
+
// loaded ad(s) too so every ad this host loaded is destroyed before the
|
|
760
|
+
// loader itself goes away; otherwise the SDK-side ad leaks.
|
|
761
|
+
CLXNativeAdLoader *loader = _nativeAdLoader;
|
|
762
|
+
CLXAd *pendingAd = _pendingManualAd;
|
|
763
|
+
CLXAd *templateAd = _currentTemplateAd;
|
|
764
|
+
if ( !loader ) return;
|
|
765
|
+
|
|
766
|
+
loader.nativeAdDelegate = nil;
|
|
767
|
+
loader.revenueDelegate = nil;
|
|
768
|
+
|
|
769
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
770
|
+
if ( pendingAd ) [loader destroyAd: pendingAd];
|
|
771
|
+
if ( templateAd && templateAd != pendingAd ) [loader destroyAd: templateAd];
|
|
772
|
+
[loader destroy];
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#pragma mark - Event payloads
|
|
777
|
+
|
|
778
|
+
- (NSDictionary<NSString *, id> *)adInfoDictForAd:(CLXAd *)ad
|
|
779
|
+
{
|
|
780
|
+
NSString *adValuesJson = @"";
|
|
781
|
+
if ( ad.adValues )
|
|
782
|
+
{
|
|
783
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject: ad.adValues options: 0 error: nil];
|
|
784
|
+
if ( data )
|
|
785
|
+
{
|
|
786
|
+
adValuesJson = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding] ?: @"";
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
NSString *nativeAdJson = @"";
|
|
791
|
+
if ( ad.nativeAd )
|
|
792
|
+
{
|
|
793
|
+
NSDictionary<NSString *, id> *metadata = [[CloudX shared] nativeAdMetadataForAd: ad];
|
|
794
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject: metadata options: 0 error: nil];
|
|
795
|
+
if ( data )
|
|
796
|
+
{
|
|
797
|
+
nativeAdJson = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding] ?: @"";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return @{@"adUnitId": ad.adUnitId ?: @"",
|
|
802
|
+
@"adFormat": @"NATIVE",
|
|
803
|
+
@"networkName": ad.networkName ?: @"",
|
|
804
|
+
@"networkPlacement": ad.networkPlacement ?: @"",
|
|
805
|
+
@"placement": ad.placement ?: @"",
|
|
806
|
+
@"creativeId": ad.creativeIdentifier ?: @"",
|
|
807
|
+
@"revenue": ad.revenue ?: @(0),
|
|
808
|
+
@"adValuesJson": adValuesJson,
|
|
809
|
+
@"nativeAdJson": nativeAdJson};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
- (NSDictionary<NSString *, id> *)loadFailedDictForAdUnit:(NSString *)adUnitId error:(nullable CLXError *)error
|
|
813
|
+
{
|
|
814
|
+
return @{@"adUnitId": adUnitId ?: (self.adUnitId ?: @""),
|
|
815
|
+
@"code": error ? @(error.code) : @(CLXErrorCodeInternalError),
|
|
816
|
+
@"message": error.localizedDescription ?: @""};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
#pragma mark - CLXNativeAdDelegate
|
|
820
|
+
|
|
821
|
+
- (void)didLoadNativeAd:(nullable CLXNativeAdView *)nativeAdView forAd:(CLXAd *)ad
|
|
822
|
+
{
|
|
823
|
+
// Snapshot the generation before the hop; see loadGeneration.
|
|
824
|
+
NSUInteger generation = self.loadGeneration;
|
|
825
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
826
|
+
if ( generation != self.loadGeneration ) return;
|
|
827
|
+
|
|
828
|
+
// Capture the ads being replaced before overwriting them; every
|
|
829
|
+
// loaded ad must be destroyed or it leaks.
|
|
830
|
+
CLXAd *prevPending = self.pendingManualAd;
|
|
831
|
+
CLXAd *prevTemplate = self.currentTemplateAd;
|
|
832
|
+
// The generation guard above means this host was not torn down, so the
|
|
833
|
+
// loader that produced this callback is still the current one. Reading
|
|
834
|
+
// it directly avoids resolveLoader lazily building a replacement.
|
|
835
|
+
CLXNativeAdLoader *loader = self.nativeAdLoader;
|
|
836
|
+
|
|
837
|
+
self.pendingManualAd = ad;
|
|
838
|
+
[self.clickableViews removeAllObjects];
|
|
839
|
+
[self.assetViewsByRole removeAllObjects];
|
|
840
|
+
[self removeSDKInsertedSubviews];
|
|
841
|
+
|
|
842
|
+
// Drop the previous ad's container tap path along with its other
|
|
843
|
+
// bindings: the replacement ad may not be container-clickable. It is
|
|
844
|
+
// reinstalled by the next successful interaction registration.
|
|
845
|
+
if ( self.containerClickRecognizer )
|
|
846
|
+
{
|
|
847
|
+
[self removeGestureRecognizer: self.containerClickRecognizer];
|
|
848
|
+
self.containerClickRecognizer = nil;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Destroy the ad(s) being replaced. Guard against destroying the
|
|
852
|
+
// freshly-loaded ad and against double-destroying the same object
|
|
853
|
+
// (pending and template can reference the same CLXAd in template mode).
|
|
854
|
+
if ( prevPending && prevPending != ad && loader ) [loader destroyAd: prevPending];
|
|
855
|
+
if ( prevTemplate && prevTemplate != ad && prevTemplate != prevPending && loader ) [loader destroyAd: prevTemplate];
|
|
856
|
+
|
|
857
|
+
if ( !self.manualRender )
|
|
858
|
+
{
|
|
859
|
+
// Template mode: render the server-provided view or build one
|
|
860
|
+
// from the publisher-selected template. renderNativeAdView:
|
|
861
|
+
// wires up impression and click tracking on the template's
|
|
862
|
+
// typed outlets.
|
|
863
|
+
CLXNativeAdView *view = nativeAdView;
|
|
864
|
+
BOOL rendered = YES;
|
|
865
|
+
if ( !view )
|
|
866
|
+
{
|
|
867
|
+
NSString *name = self.templateName.length > 0 ? self.templateName : @"medium_template";
|
|
868
|
+
view = [CLXNativeAdView viewFromAd: ad.nativeAd withTemplate: name];
|
|
869
|
+
rendered = loader ? [loader renderNativeAdView: view withAd: ad] : NO;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if ( !rendered )
|
|
873
|
+
{
|
|
874
|
+
// Template render failed — the ad is expired or invalid. Do
|
|
875
|
+
// not add the view, do not fire onAdLoadedEvent; surface the
|
|
876
|
+
// failure so it is observable.
|
|
877
|
+
[self.currentNativeView removeFromSuperview];
|
|
878
|
+
self.currentNativeView = nil;
|
|
879
|
+
self.currentTemplateAd = nil;
|
|
880
|
+
[[CloudX shared] log: @"Native template render failed (ad expired or invalid) — emitting onAdLoadFailedEvent. adUnitId=%@", ad.adUnitId ?: @""];
|
|
881
|
+
CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterAdExpired
|
|
882
|
+
description: @"template render failed; ad expired or invalid"];
|
|
883
|
+
if ( self.onAdLoadFailedEvent ) self.onAdLoadFailedEvent([self loadFailedDictForAdUnit: ad.adUnitId ?: @"" error: error]);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
[self.currentNativeView removeFromSuperview];
|
|
888
|
+
self.currentNativeView = view;
|
|
889
|
+
self.currentTemplateAd = ad;
|
|
890
|
+
|
|
891
|
+
view.translatesAutoresizingMaskIntoConstraints = NO;
|
|
892
|
+
[self addSubview: view];
|
|
893
|
+
[NSLayoutConstraint activateConstraints: @[
|
|
894
|
+
[view.topAnchor constraintEqualToAnchor: self.topAnchor],
|
|
895
|
+
[view.bottomAnchor constraintEqualToAnchor: self.bottomAnchor],
|
|
896
|
+
[view.leadingAnchor constraintEqualToAnchor: self.leadingAnchor],
|
|
897
|
+
[view.trailingAnchor constraintEqualToAnchor: self.trailingAnchor],
|
|
898
|
+
]];
|
|
899
|
+
}
|
|
900
|
+
else
|
|
901
|
+
{
|
|
902
|
+
// Manual mode: the publisher's children drive the visual layout. We
|
|
903
|
+
// hold pendingManualAd so the subsequent updateAssetView +
|
|
904
|
+
// renderNativeAd commands can populate text, attach media /
|
|
905
|
+
// options views, and register click + impression tracking against
|
|
906
|
+
// the publisher's subviews.
|
|
907
|
+
self.currentTemplateAd = nil;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if ( self.onAdLoadedEvent ) self.onAdLoadedEvent([self adInfoDictForAd: ad]);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
- (void)didFailToLoadNativeAdForAdUnitIdentifier:(NSString *)adUnitId error:(CLXError *)error
|
|
915
|
+
{
|
|
916
|
+
// Snapshot the generation before the hop; see loadGeneration.
|
|
917
|
+
NSUInteger generation = self.loadGeneration;
|
|
918
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
919
|
+
if ( generation != self.loadGeneration ) return;
|
|
920
|
+
self.hasRequestedLoad = NO;
|
|
921
|
+
// The template-render failure path logs; this one did not, so a load
|
|
922
|
+
// failure reached JS while leaving no trace in the device log.
|
|
923
|
+
[[CloudX shared] log: @"Native ad load failed. adUnitId=%@ code=%ld message=%@",
|
|
924
|
+
adUnitId ?: @"", (long)(error ? error.code : CLXErrorCodeInternalError),
|
|
925
|
+
error.localizedDescription ?: @""];
|
|
926
|
+
if ( self.onAdLoadFailedEvent ) self.onAdLoadFailedEvent([self loadFailedDictForAdUnit: adUnitId error: error]);
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
- (void)didClickNativeAd:(CLXAd *)ad
|
|
931
|
+
{
|
|
932
|
+
// Snapshot the generation before the hop; see loadGeneration.
|
|
933
|
+
NSUInteger generation = self.loadGeneration;
|
|
934
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
935
|
+
if ( generation != self.loadGeneration ) return;
|
|
936
|
+
if ( self.onAdClickedEvent ) self.onAdClickedEvent([self adInfoDictForAd: ad]);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
- (void)didExpireNativeAd:(CLXAd *)ad
|
|
941
|
+
{
|
|
942
|
+
// Snapshot the generation before the hop; see loadGeneration.
|
|
943
|
+
NSUInteger generation = self.loadGeneration;
|
|
944
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
945
|
+
if ( generation != self.loadGeneration ) return;
|
|
946
|
+
if ( self.onAdExpiredEvent ) self.onAdExpiredEvent([self adInfoDictForAd: ad]);
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
- (void)didCloseNativeAd:(CLXAd *)ad
|
|
951
|
+
{
|
|
952
|
+
// Snapshot the generation before the hop; see loadGeneration.
|
|
953
|
+
NSUInteger generation = self.loadGeneration;
|
|
954
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
955
|
+
if ( generation != self.loadGeneration ) return;
|
|
956
|
+
if ( self.onAdClosedEvent ) self.onAdClosedEvent([self adInfoDictForAd: ad]);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
#pragma mark - CLXAdRevenueDelegate
|
|
961
|
+
|
|
962
|
+
- (void)didPayRevenueForAd:(CLXAd *)ad
|
|
963
|
+
{
|
|
964
|
+
// Snapshot the generation before the hop; see loadGeneration.
|
|
965
|
+
NSUInteger generation = self.loadGeneration;
|
|
966
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
967
|
+
if ( generation != self.loadGeneration ) return;
|
|
968
|
+
if ( self.onAdRevenuePaidEvent ) self.onAdRevenuePaidEvent([self adInfoDictForAd: ad]);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@end
|
|
973
|
+
|
|
974
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
975
|
+
Class<RCTComponentViewProtocol> CloudXNativeAdViewCls(void)
|
|
976
|
+
{
|
|
977
|
+
return [CloudXNativeAdView class];
|
|
978
|
+
}
|
|
979
|
+
#endif
|