cloudx-react-native 3.10.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +1 -0
  2. package/android/src/main/java/com/cloudxreactnative/CloudXAdEvents.kt +10 -0
  3. package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +40 -0
  4. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +718 -0
  5. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +119 -0
  6. package/android/src/main/java/com/cloudxreactnative/CloudXPackage.kt +4 -1
  7. package/android/src/main/res/layout/cloudx_rn_native_media_banner_template.xml +100 -0
  8. package/android/src/main/res/layout/cloudx_rn_native_medium_template.xml +99 -0
  9. package/android/src/main/res/layout/cloudx_rn_native_small_template.xml +72 -0
  10. package/android/src/main/res/layout/cloudx_rn_native_vertical_media_template.xml +117 -0
  11. package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +1 -0
  12. package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +86 -0
  13. package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +1 -0
  14. package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +118 -0
  15. package/ios/CloudX.h +8 -0
  16. package/ios/CloudX.mm +159 -29
  17. package/ios/CloudXNativeAdView.h +75 -0
  18. package/ios/CloudXNativeAdView.mm +979 -0
  19. package/ios/CloudXNativeAdViewManager.h +12 -0
  20. package/ios/CloudXNativeAdViewManager.mm +119 -0
  21. package/lib/commonjs/CloudXAdViewBase.js +8 -97
  22. package/lib/commonjs/CloudXAdViewBase.js.map +1 -1
  23. package/lib/commonjs/CloudXNativeAdView.js +331 -0
  24. package/lib/commonjs/CloudXNativeAdView.js.map +1 -0
  25. package/lib/commonjs/CloudXNativeAdViewComponents.js +252 -0
  26. package/lib/commonjs/CloudXNativeAdViewComponents.js.map +1 -0
  27. package/lib/commonjs/CloudXNativeAdViewProvider.js +117 -0
  28. package/lib/commonjs/CloudXNativeAdViewProvider.js.map +1 -0
  29. package/lib/commonjs/extraParameters.js +52 -0
  30. package/lib/commonjs/extraParameters.js.map +1 -0
  31. package/lib/commonjs/hooks.js +34 -0
  32. package/lib/commonjs/hooks.js.map +1 -1
  33. package/lib/commonjs/index.js +72 -0
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +41 -0
  36. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
  37. package/lib/commonjs/types/NativeAd.js +70 -0
  38. package/lib/commonjs/types/NativeAd.js.map +1 -0
  39. package/lib/commonjs/types/index.js +13 -0
  40. package/lib/commonjs/types/index.js.map +1 -1
  41. package/lib/commonjs/useCloudXSdkInitialization.js +75 -0
  42. package/lib/commonjs/useCloudXSdkInitialization.js.map +1 -0
  43. package/lib/commonjs/version.js +1 -1
  44. package/lib/module/CloudXAdViewBase.js +7 -96
  45. package/lib/module/CloudXAdViewBase.js.map +1 -1
  46. package/lib/module/CloudXNativeAdView.js +324 -0
  47. package/lib/module/CloudXNativeAdView.js.map +1 -0
  48. package/lib/module/CloudXNativeAdViewComponents.js +240 -0
  49. package/lib/module/CloudXNativeAdViewComponents.js.map +1 -0
  50. package/lib/module/CloudXNativeAdViewProvider.js +110 -0
  51. package/lib/module/CloudXNativeAdViewProvider.js.map +1 -0
  52. package/lib/module/extraParameters.js +45 -0
  53. package/lib/module/extraParameters.js.map +1 -0
  54. package/lib/module/hooks.js +34 -0
  55. package/lib/module/hooks.js.map +1 -1
  56. package/lib/module/index.js +13 -0
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +35 -0
  59. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
  60. package/lib/module/types/NativeAd.js +68 -0
  61. package/lib/module/types/NativeAd.js.map +1 -0
  62. package/lib/module/types/index.js +1 -0
  63. package/lib/module/types/index.js.map +1 -1
  64. package/lib/module/useCloudXSdkInitialization.js +68 -0
  65. package/lib/module/useCloudXSdkInitialization.js.map +1 -0
  66. package/lib/module/version.js +1 -1
  67. package/lib/typescript/src/CloudXAdViewBase.d.ts.map +1 -1
  68. package/lib/typescript/src/CloudXNativeAdView.d.ts +24 -0
  69. package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -0
  70. package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts +67 -0
  71. package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts.map +1 -0
  72. package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts +72 -0
  73. package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts.map +1 -0
  74. package/lib/typescript/src/extraParameters.d.ts +4 -0
  75. package/lib/typescript/src/extraParameters.d.ts.map +1 -0
  76. package/lib/typescript/src/hooks.d.ts.map +1 -1
  77. package/lib/typescript/src/index.d.ts +4 -1
  78. package/lib/typescript/src/index.d.ts.map +1 -1
  79. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +103 -0
  80. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -0
  81. package/lib/typescript/src/types/AdInfo.d.ts +4 -1
  82. package/lib/typescript/src/types/AdInfo.d.ts.map +1 -1
  83. package/lib/typescript/src/types/NativeAd.d.ts +114 -0
  84. package/lib/typescript/src/types/NativeAd.d.ts.map +1 -0
  85. package/lib/typescript/src/types/index.d.ts +2 -0
  86. package/lib/typescript/src/types/index.d.ts.map +1 -1
  87. package/lib/typescript/src/useCloudXSdkInitialization.d.ts +16 -0
  88. package/lib/typescript/src/useCloudXSdkInitialization.d.ts.map +1 -0
  89. package/lib/typescript/src/version.d.ts +1 -1
  90. package/package.json +1 -1
  91. package/src/CloudXAdViewBase.tsx +8 -125
  92. package/src/CloudXNativeAdView.tsx +419 -0
  93. package/src/CloudXNativeAdViewComponents.tsx +203 -0
  94. package/src/CloudXNativeAdViewProvider.tsx +149 -0
  95. package/src/extraParameters.ts +65 -0
  96. package/src/hooks.ts +38 -0
  97. package/src/index.ts +24 -0
  98. package/src/specs/CloudXNativeAdViewNativeComponent.ts +137 -0
  99. package/src/types/AdInfo.ts +5 -1
  100. package/src/types/NativeAd.ts +125 -0
  101. package/src/types/index.ts +7 -0
  102. package/src/useCloudXSdkInitialization.ts +73 -0
  103. 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