cloudx-react-native 3.10.1 → 3.11.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 (78) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/com/cloudxreactnative/CloudXAdEvents.kt +9 -0
  3. package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +76 -0
  4. package/android/src/main/java/com/cloudxreactnative/CloudXModuleImpl.kt +110 -0
  5. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdHolder.kt +184 -0
  6. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +209 -70
  7. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +6 -0
  8. package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +9 -0
  9. package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +2 -0
  10. package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +14 -0
  11. package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +3 -0
  12. package/android/src/test/java/com/cloudxreactnative/CloudXNativeAdPreloadTest.kt +351 -0
  13. package/android/src/test/java/com/cloudxreactnative/FakeReadable.kt +2 -2
  14. package/cloudx-react-native.podspec +1 -1
  15. package/ios/CloudX.h +12 -0
  16. package/ios/CloudX.mm +94 -1
  17. package/ios/CloudXNativeAdHolder.h +66 -0
  18. package/ios/CloudXNativeAdHolder.mm +247 -0
  19. package/ios/CloudXNativeAdView.h +9 -0
  20. package/ios/CloudXNativeAdView.mm +308 -64
  21. package/ios/CloudXNativeAdViewManager.mm +7 -0
  22. package/lib/commonjs/CloudXNativeAd.js +178 -0
  23. package/lib/commonjs/CloudXNativeAd.js.map +1 -0
  24. package/lib/commonjs/CloudXNativeAdView.js +58 -11
  25. package/lib/commonjs/CloudXNativeAdView.js.map +1 -1
  26. package/lib/commonjs/hooks.js +203 -0
  27. package/lib/commonjs/hooks.js.map +1 -1
  28. package/lib/commonjs/index.js +13 -0
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +2 -0
  31. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
  32. package/lib/commonjs/specs/NativeCloudX.js.map +1 -1
  33. package/lib/commonjs/types/NativeAd.js +21 -1
  34. package/lib/commonjs/types/NativeAd.js.map +1 -1
  35. package/lib/commonjs/types/index.js.map +1 -1
  36. package/lib/commonjs/version.js +1 -1
  37. package/lib/module/CloudXNativeAd.js +170 -0
  38. package/lib/module/CloudXNativeAd.js.map +1 -0
  39. package/lib/module/CloudXNativeAdView.js +58 -11
  40. package/lib/module/CloudXNativeAdView.js.map +1 -1
  41. package/lib/module/hooks.js +202 -0
  42. package/lib/module/hooks.js.map +1 -1
  43. package/lib/module/index.js +2 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +2 -0
  46. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
  47. package/lib/module/specs/NativeCloudX.js.map +1 -1
  48. package/lib/module/types/NativeAd.js +26 -1
  49. package/lib/module/types/NativeAd.js.map +1 -1
  50. package/lib/module/types/index.js.map +1 -1
  51. package/lib/module/version.js +1 -1
  52. package/lib/typescript/src/CloudXNativeAd.d.ts +58 -0
  53. package/lib/typescript/src/CloudXNativeAd.d.ts.map +1 -0
  54. package/lib/typescript/src/CloudXNativeAdView.d.ts +6 -13
  55. package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -1
  56. package/lib/typescript/src/hooks.d.ts +30 -0
  57. package/lib/typescript/src/hooks.d.ts.map +1 -1
  58. package/lib/typescript/src/index.d.ts +4 -3
  59. package/lib/typescript/src/index.d.ts.map +1 -1
  60. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +23 -2
  61. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -1
  62. package/lib/typescript/src/specs/NativeCloudX.d.ts +13 -0
  63. package/lib/typescript/src/specs/NativeCloudX.d.ts.map +1 -1
  64. package/lib/typescript/src/types/NativeAd.d.ts +76 -13
  65. package/lib/typescript/src/types/NativeAd.d.ts.map +1 -1
  66. package/lib/typescript/src/types/index.d.ts +1 -1
  67. package/lib/typescript/src/types/index.d.ts.map +1 -1
  68. package/lib/typescript/src/version.d.ts +1 -1
  69. package/package.json +1 -1
  70. package/src/CloudXNativeAd.ts +177 -0
  71. package/src/CloudXNativeAdView.tsx +82 -15
  72. package/src/hooks.ts +236 -0
  73. package/src/index.ts +14 -1
  74. package/src/specs/CloudXNativeAdViewNativeComponent.ts +27 -2
  75. package/src/specs/NativeCloudX.ts +20 -0
  76. package/src/types/NativeAd.ts +85 -18
  77. package/src/types/index.ts +4 -0
  78. package/src/version.ts +1 -1
@@ -5,6 +5,7 @@
5
5
 
6
6
  #import "CloudXNativeAdView.h"
7
7
  #import "CloudX.h"
8
+ #import "CloudXNativeAdHolder.h"
8
9
  #import <React/RCTLog.h>
9
10
  // Supplies `UIView.reactTag`, which Paper uses instead of `UIView.tag`.
10
11
  #import <React/UIView+React.h>
@@ -144,6 +145,12 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
144
145
  @property (nonatomic, strong) NSMutableArray<UIView *> *insertedSDKSubviews;
145
146
 
146
147
  @property (nonatomic, assign) BOOL hasRequestedLoad;
148
+ @property (nonatomic, assign) BOOL ownsLoader;
149
+ @property (nonatomic, weak, nullable) CloudXNativeAdHolder *boundHolder;
150
+ // Set once a preloaded ad has been presented here (template rendered, or the
151
+ // manual bind handed to JS), so a window re-attach (tab switch, screen
152
+ // transition) keeps the existing presentation instead of rendering again.
153
+ @property (nonatomic, assign) BOOL preloadedAdPresented;
147
154
 
148
155
  @end
149
156
 
@@ -196,6 +203,25 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
196
203
  emitter->onAdLoadedEvent(result);
197
204
  };
198
205
 
206
+ self.onAdDisplayedEvent = ^(NSDictionary *event) {
207
+ CloudXNativeAdView *strongSelf = weakSelf;
208
+ if ( !strongSelf || !strongSelf->_eventEmitter ) return;
209
+ auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
210
+ CloudXNativeAdViewEventEmitter::OnAdDisplayedEvent result;
211
+ CLX_FILL_NATIVE_ADINFO(result, event);
212
+ emitter->onAdDisplayedEvent(result);
213
+ };
214
+
215
+ self.onAdBindingClearedEvent = ^(NSDictionary *event) {
216
+ CloudXNativeAdView *strongSelf = weakSelf;
217
+ if ( !strongSelf || !strongSelf->_eventEmitter ) return;
218
+ auto emitter = std::static_pointer_cast<CloudXNativeAdViewEventEmitter const>(strongSelf->_eventEmitter);
219
+ CloudXNativeAdViewEventEmitter::OnAdBindingClearedEvent result;
220
+ NSString *clearedId = [event[@"nativeAdId"] isKindOfClass: [NSString class]] ? event[@"nativeAdId"] : @"";
221
+ result.nativeAdId = std::string([clearedId UTF8String] ?: "");
222
+ emitter->onAdBindingClearedEvent(result);
223
+ };
224
+
199
225
  self.onAdClickedEvent = ^(NSDictionary *event) {
200
226
  CloudXNativeAdView *strongSelf = weakSelf;
201
227
  if ( !strongSelf || !strongSelf->_eventEmitter ) return;
@@ -264,6 +290,10 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
264
290
  {
265
291
  self.adUnitId = RCTNSStringFromString(newViewProps.adUnitId);
266
292
  }
293
+ if ( oldViewProps.nativeAdId != newViewProps.nativeAdId )
294
+ {
295
+ self.nativeAdId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeAdId);
296
+ }
267
297
  if ( oldViewProps.templateName != newViewProps.templateName )
268
298
  {
269
299
  self.templateName = RCTNSStringFromStringNilIfEmpty(newViewProps.templateName);
@@ -323,9 +353,18 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
323
353
  // persistent, stateful per-instance object. A recycled Fabric view must
324
354
  // never leak the prior mount's ad/loader state into the next mount.
325
355
  // destroyAd detaches the delegates, destroys the loader and nils it.
326
- [self destroyAd];
356
+ if ( self.nativeAdId.length > 0 )
357
+ {
358
+ [self teardownPreloadedBinding];
359
+ }
360
+ else
361
+ {
362
+ [self destroyAd];
363
+ }
327
364
 
365
+ self.ownsLoader = NO;
328
366
  self.adUnitId = nil;
367
+ self.nativeAdId = nil;
329
368
  self.templateName = nil;
330
369
  self.placement = nil;
331
370
  self.customData = nil;
@@ -354,11 +393,41 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
354
393
 
355
394
  #pragma mark - Property setters (shared; Paper view manager sets these via KVC)
356
395
 
396
+ - (void)setNativeAdId:(nullable NSString *)nativeAdId
397
+ {
398
+ NSString *normalized = (nativeAdId.length > 0) ? [nativeAdId copy] : nil;
399
+ if ( [normalized isEqual: _nativeAdId] ) return;
400
+
401
+ [self teardownPreloadedBinding];
402
+
403
+ _nativeAdId = normalized;
404
+ if ( _nativeAdId.length == 0 ) return;
405
+
406
+ // Resolve holder immediately so manual-mode commands find pendingManualAd even before attach
407
+ CloudXNativeAdHolder *holder = [CloudXNativeAdHolder holderForId: _nativeAdId];
408
+ if ( holder )
409
+ {
410
+ self.pendingManualAd = holder.ad;
411
+ self.boundHolder = holder;
412
+ }
413
+
414
+ if ( self.window )
415
+ {
416
+ [self bindPreloadedAdIfNeeded];
417
+ }
418
+ }
419
+
357
420
  - (void)setAdUnitId:(NSString *)adUnitId
358
421
  {
359
422
  NSString *normalized = [adUnitId copy];
360
423
  if ( [normalized isEqual: _adUnitId] ) return;
361
424
 
425
+ if ( self.nativeAdId.length > 0 )
426
+ {
427
+ _adUnitId = normalized;
428
+ return;
429
+ }
430
+
362
431
  // First-time set (no loader yet): just store the value. didMoveToWindow /
363
432
  // loadIfNeeded loads once the view is attached.
364
433
  if ( !self.nativeAdLoader )
@@ -411,7 +480,17 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
411
480
  // hierarchy, so manualRender is settled by the time this callback fires.
412
481
  // Auto-load runs in both render modes; the mode only controls how
413
482
  // didLoadNativeAd:forAd: presents the result.
414
- if ( self.window ) [self loadIfNeeded];
483
+ if ( self.window )
484
+ {
485
+ if ( self.nativeAdId.length > 0 )
486
+ {
487
+ [self bindPreloadedAdIfNeeded];
488
+ }
489
+ else
490
+ {
491
+ [self loadIfNeeded];
492
+ }
493
+ }
415
494
  }
416
495
 
417
496
  - (CLXNativeAdLoader *)resolveLoader
@@ -425,6 +504,7 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
425
504
  self.nativeAdLoader = [[CloudXCore shared] createNativeAdLoaderWithAdUnitIdentifier: self.adUnitId];
426
505
  self.nativeAdLoader.nativeAdDelegate = self;
427
506
  self.nativeAdLoader.revenueDelegate = self;
507
+ self.ownsLoader = YES;
428
508
  }
429
509
 
430
510
  return self.nativeAdLoader;
@@ -432,50 +512,25 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
432
512
 
433
513
  - (void)loadIfNeeded
434
514
  {
515
+ if ( self.nativeAdId.length > 0 ) return;
435
516
  if ( self.hasRequestedLoad || self.adUnitId.length == 0 ) return;
436
517
 
437
518
  CLXNativeAdLoader *loader = [self resolveLoader];
438
519
  if ( !loader ) return;
439
520
 
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];
521
+ [CloudXNativeAdHolder configureLoader: loader
522
+ placement: self.placement
523
+ customData: self.customData
524
+ disableVideoFullScreen: self.disableVideoFullScreen
525
+ startVideoUnmuted: self.startVideoUnmuted
526
+ hideVideoMediaControls: self.hideVideoMediaControls
527
+ extraParameterEntries: self.extraParameterEntries
528
+ localExtraParameterEntries: self.localExtraParameterEntries];
447
529
 
448
530
  self.hasRequestedLoad = YES;
449
531
  [loader loadAd];
450
532
  }
451
533
 
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
534
  #pragma mark - Commands
480
535
  //
481
536
  // Method names match RCTCloudXNativeAdViewViewProtocol exactly, so Fabric's
@@ -485,6 +540,7 @@ static NSArray<NSDictionary<NSString *, id> *> *CLXEntryArrayFromNativeAdViewStr
485
540
 
486
541
  - (void)loadAd
487
542
  {
543
+ if ( self.nativeAdId.length > 0 ) return;
488
544
  // Explicit JS-driven (re)load bypasses the "already requested" guard used
489
545
  // by the didMoveToWindow auto-load.
490
546
  self.hasRequestedLoad = NO;
@@ -636,6 +692,14 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
636
692
  if ( registered )
637
693
  {
638
694
  [self clx_updateContainerClickRecognizerForAd: ad];
695
+ if ( self.boundHolder )
696
+ {
697
+ self.boundHolder.boundView = self;
698
+ }
699
+ if ( self.onAdDisplayedEvent )
700
+ {
701
+ self.onAdDisplayedEvent([self adInfoDictForAd: ad]);
702
+ }
639
703
  return;
640
704
  }
641
705
 
@@ -696,6 +760,9 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
696
760
 
697
761
  - (void)destroyAd
698
762
  {
763
+ if ( self.nativeAdId.length > 0 ) return;
764
+ if ( !self.ownsLoader ) return;
765
+
699
766
  // Invalidate first: a load already in flight belongs to the generation
700
767
  // being torn down, and its callback must not render into this host.
701
768
  self.loadGeneration++;
@@ -752,6 +819,16 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
752
819
 
753
820
  - (void)dealloc
754
821
  {
822
+ if ( !_ownsLoader )
823
+ {
824
+ CloudXNativeAdHolder *holder = _boundHolder;
825
+ if ( holder && holder.boundView == self )
826
+ {
827
+ holder.boundView = nil;
828
+ }
829
+ return;
830
+ }
831
+
755
832
  // The loader is owned by this host. Destroying it releases the SDK-side
756
833
  // ad pipeline and cancels any in-flight auction so nothing calls back
757
834
  // into the zombie delegate. Keep strong local references across the
@@ -773,6 +850,195 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
773
850
  });
774
851
  }
775
852
 
853
+ #pragma mark - Preloaded ad binding
854
+
855
+ - (BOOL)presentTemplateAd:(CLXAd *)ad
856
+ withNativeAdView:(nullable CLXNativeAdView *)nativeAdView
857
+ loader:(nullable CLXNativeAdLoader *)loader
858
+ {
859
+ CLXNativeAdView *view = nativeAdView;
860
+ BOOL rendered = YES;
861
+ if ( !view )
862
+ {
863
+ NSString *name = self.templateName.length > 0 ? self.templateName : @"medium_template";
864
+ view = [CLXNativeAdView viewFromAd: ad.nativeAd withTemplate: name];
865
+ rendered = loader ? [loader renderNativeAdView: view withAd: ad] : NO;
866
+ }
867
+
868
+ if ( !rendered )
869
+ {
870
+ [self.currentNativeView removeFromSuperview];
871
+ self.currentNativeView = nil;
872
+ self.currentTemplateAd = nil;
873
+ return NO;
874
+ }
875
+
876
+ [self.currentNativeView removeFromSuperview];
877
+ self.currentNativeView = view;
878
+ self.currentTemplateAd = ad;
879
+
880
+ view.translatesAutoresizingMaskIntoConstraints = NO;
881
+ [self addSubview: view];
882
+ [NSLayoutConstraint activateConstraints: @[
883
+ [view.topAnchor constraintEqualToAnchor: self.topAnchor],
884
+ [view.bottomAnchor constraintEqualToAnchor: self.bottomAnchor],
885
+ [view.leadingAnchor constraintEqualToAnchor: self.leadingAnchor],
886
+ [view.trailingAnchor constraintEqualToAnchor: self.trailingAnchor],
887
+ ]];
888
+ return YES;
889
+ }
890
+
891
+ - (void)clearRenderedAdSilently
892
+ {
893
+ // A preloaded ad leaving this view also takes its React-rendered text and
894
+ // images with it: tell the JS view so its asset components clear.
895
+ BOOL hadPreloadedAd = self.nativeAdId.length > 0 &&
896
+ (self.preloadedAdPresented || self.pendingManualAd || self.currentTemplateAd);
897
+ if ( hadPreloadedAd && self.onAdBindingClearedEvent )
898
+ {
899
+ self.onAdBindingClearedEvent(@{ @"nativeAdId": self.nativeAdId });
900
+ }
901
+ self.preloadedAdPresented = NO;
902
+ self.pendingManualAd = nil;
903
+ self.currentTemplateAd = nil;
904
+
905
+ [self.currentNativeView removeFromSuperview];
906
+ self.currentNativeView = nil;
907
+
908
+ [self removeSDKInsertedSubviews];
909
+ [self.clickableViews removeAllObjects];
910
+ [self.assetViewsByRole removeAllObjects];
911
+
912
+ if ( self.containerClickRecognizer )
913
+ {
914
+ [self removeGestureRecognizer: self.containerClickRecognizer];
915
+ self.containerClickRecognizer = nil;
916
+ }
917
+ }
918
+
919
+ - (void)handleBoundAdDestroyed
920
+ {
921
+ NSString *adUnitId = self.boundHolder.adUnitId ?: (self.adUnitId ?: @"");
922
+ [self clearRenderedAdSilently];
923
+ self.boundHolder = nil;
924
+
925
+ CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdNotReady
926
+ description: @"native ad destroyed"];
927
+ if ( self.onAdDisplayFailedEvent )
928
+ {
929
+ self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: adUnitId error: error]);
930
+ }
931
+ }
932
+
933
+ - (void)handleBoundAdExpired
934
+ {
935
+ NSString *adUnitId = self.boundHolder.adUnitId ?: (self.adUnitId ?: @"");
936
+ [self clearRenderedAdSilently];
937
+ self.boundHolder = nil;
938
+
939
+ CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterAdExpired
940
+ description: @"native ad expired"];
941
+ if ( self.onAdDisplayFailedEvent )
942
+ {
943
+ self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: adUnitId error: error]);
944
+ }
945
+ }
946
+
947
+ - (void)teardownPreloadedBinding
948
+ {
949
+ if ( self.boundHolder )
950
+ {
951
+ if ( self.boundHolder.boundView == self )
952
+ {
953
+ self.boundHolder.boundView = nil;
954
+ }
955
+ self.boundHolder = nil;
956
+ }
957
+ [self clearRenderedAdSilently];
958
+ }
959
+
960
+ - (void)bindPreloadedAdIfNeeded
961
+ {
962
+ if ( self.nativeAdId.length == 0 ) return;
963
+
964
+ CloudXNativeAdHolder *holder = [CloudXNativeAdHolder holderForId: self.nativeAdId];
965
+ if ( !holder )
966
+ {
967
+ CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdNotReady
968
+ description: @"native ad destroyed"];
969
+ if ( self.onAdDisplayFailedEvent )
970
+ {
971
+ self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: self.adUnitId ?: @"" error: error]);
972
+ }
973
+ return;
974
+ }
975
+
976
+ if ( self.preloadedAdPresented && holder == self.boundHolder && holder.boundView == self ) return;
977
+
978
+ if ( holder.isExpired || (holder.ad && holder.ad.nativeAd && holder.ad.nativeAd.isExpired) )
979
+ {
980
+ CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterAdExpired
981
+ description: @"native ad expired"];
982
+ if ( self.onAdDisplayFailedEvent )
983
+ {
984
+ self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: holder.adUnitId ?: (self.adUnitId ?: @"") error: error]);
985
+ }
986
+ return;
987
+ }
988
+
989
+ if ( !holder.ad )
990
+ {
991
+ CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdNotReady
992
+ description: @"native ad destroyed"];
993
+ if ( self.onAdDisplayFailedEvent )
994
+ {
995
+ self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: holder.adUnitId ?: (self.adUnitId ?: @"") error: error]);
996
+ }
997
+ return;
998
+ }
999
+
1000
+ // Latest bind wins: if another view is currently bound, clear it silently
1001
+ CloudXNativeAdView *previousBound = holder.boundView;
1002
+ if ( previousBound && previousBound != self )
1003
+ {
1004
+ [previousBound clearRenderedAdSilently];
1005
+ }
1006
+ holder.boundView = self;
1007
+ self.boundHolder = holder;
1008
+ self.pendingManualAd = holder.ad;
1009
+
1010
+ if ( !self.manualRender )
1011
+ {
1012
+ BOOL rendered = [self presentTemplateAd: holder.ad withNativeAdView: nil loader: holder.loader];
1013
+ if ( !rendered )
1014
+ {
1015
+ [[CloudX shared] log: @"Preloaded native template render failed (ad expired or invalid) — emitting onAdDisplayFailedEvent. nativeAdId=%@", self.nativeAdId];
1016
+ CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterAdExpired
1017
+ description: @"template render failed; ad expired or invalid"];
1018
+ if ( self.onAdDisplayFailedEvent )
1019
+ {
1020
+ self.onAdDisplayFailedEvent([self loadFailedDictForAdUnit: holder.adUnitId ?: (self.adUnitId ?: @"") error: error]);
1021
+ }
1022
+ return;
1023
+ }
1024
+
1025
+ self.preloadedAdPresented = YES;
1026
+ if ( self.onAdDisplayedEvent )
1027
+ {
1028
+ self.onAdDisplayedEvent([self adInfoDictForAd: holder.ad]);
1029
+ }
1030
+ }
1031
+ else
1032
+ {
1033
+ self.currentTemplateAd = nil;
1034
+ self.preloadedAdPresented = YES;
1035
+ if ( self.onAdLoadedEvent )
1036
+ {
1037
+ self.onAdLoadedEvent([self adInfoDictForAd: holder.ad]);
1038
+ }
1039
+ }
1040
+ }
1041
+
776
1042
  #pragma mark - Event payloads
777
1043
 
778
1044
  - (NSDictionary<NSString *, id> *)adInfoDictForAd:(CLXAd *)ad
@@ -856,27 +1122,12 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
856
1122
 
857
1123
  if ( !self.manualRender )
858
1124
  {
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
-
1125
+ BOOL rendered = [self presentTemplateAd: ad withNativeAdView: nativeAdView loader: loader];
872
1126
  if ( !rendered )
873
1127
  {
874
1128
  // Template render failed — the ad is expired or invalid. Do
875
1129
  // not add the view, do not fire onAdLoadedEvent; surface the
876
1130
  // failure so it is observable.
877
- [self.currentNativeView removeFromSuperview];
878
- self.currentNativeView = nil;
879
- self.currentTemplateAd = nil;
880
1131
  [[CloudX shared] log: @"Native template render failed (ad expired or invalid) — emitting onAdLoadFailedEvent. adUnitId=%@", ad.adUnitId ?: @""];
881
1132
  CLXError *error = [CLXError errorWithCode: CLXErrorCodeAdapterAdExpired
882
1133
  description: @"template render failed; ad expired or invalid"];
@@ -884,18 +1135,7 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
884
1135
  return;
885
1136
  }
886
1137
 
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
- ]];
1138
+ if ( self.onAdDisplayedEvent ) self.onAdDisplayedEvent([self adInfoDictForAd: ad]);
899
1139
  }
900
1140
  else
901
1141
  {
@@ -929,6 +1169,7 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
929
1169
 
930
1170
  - (void)didClickNativeAd:(CLXAd *)ad
931
1171
  {
1172
+ if ( self.nativeAdId.length > 0 ) return;
932
1173
  // Snapshot the generation before the hop; see loadGeneration.
933
1174
  NSUInteger generation = self.loadGeneration;
934
1175
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -939,6 +1180,7 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
939
1180
 
940
1181
  - (void)didExpireNativeAd:(CLXAd *)ad
941
1182
  {
1183
+ if ( self.nativeAdId.length > 0 ) return;
942
1184
  // Snapshot the generation before the hop; see loadGeneration.
943
1185
  NSUInteger generation = self.loadGeneration;
944
1186
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -949,6 +1191,7 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
949
1191
 
950
1192
  - (void)didCloseNativeAd:(CLXAd *)ad
951
1193
  {
1194
+ if ( self.nativeAdId.length > 0 ) return;
952
1195
  // Snapshot the generation before the hop; see loadGeneration.
953
1196
  NSUInteger generation = self.loadGeneration;
954
1197
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -961,6 +1204,7 @@ static UIView *clx_findDescendantWithReactTag(UIView *root, NSInteger reactTag)
961
1204
 
962
1205
  - (void)didPayRevenueForAd:(CLXAd *)ad
963
1206
  {
1207
+ if ( self.nativeAdId.length > 0 ) return;
964
1208
  // Snapshot the generation before the hop; see loadGeneration.
965
1209
  NSUInteger generation = self.loadGeneration;
966
1210
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -27,6 +27,11 @@ RCT_CUSTOM_VIEW_PROPERTY(adUnitId, NSString, CloudXNativeAdView)
27
27
  view.adUnitId = (json == nil || json == (id)[NSNull null]) ? nil : [RCTConvert NSString: json];
28
28
  }
29
29
 
30
+ RCT_CUSTOM_VIEW_PROPERTY(nativeAdId, NSString, CloudXNativeAdView)
31
+ {
32
+ view.nativeAdId = (json == nil || json == (id)[NSNull null]) ? nil : [RCTConvert NSString: json];
33
+ }
34
+
30
35
  RCT_CUSTOM_VIEW_PROPERTY(extraParameters, NSArray, CloudXNativeAdView)
31
36
  {
32
37
  NSArray<NSDictionary<NSString *, id> *> *entries = [json isKindOfClass: [NSArray class]] ? json : nil;
@@ -51,8 +56,10 @@ RCT_CUSTOM_VIEW_PROPERTY(template, NSString, CloudXNativeAdView)
51
56
  }
52
57
 
53
58
  RCT_EXPORT_VIEW_PROPERTY(onAdLoadedEvent, RCTDirectEventBlock)
59
+ RCT_EXPORT_VIEW_PROPERTY(onAdDisplayedEvent, RCTDirectEventBlock)
54
60
  RCT_EXPORT_VIEW_PROPERTY(onAdLoadFailedEvent, RCTDirectEventBlock)
55
61
  RCT_EXPORT_VIEW_PROPERTY(onAdDisplayFailedEvent, RCTDirectEventBlock)
62
+ RCT_EXPORT_VIEW_PROPERTY(onAdBindingClearedEvent, RCTDirectEventBlock)
56
63
  RCT_EXPORT_VIEW_PROPERTY(onAdClickedEvent, RCTDirectEventBlock)
57
64
  RCT_EXPORT_VIEW_PROPERTY(onAdExpiredEvent, RCTDirectEventBlock)
58
65
  RCT_EXPORT_VIEW_PROPERTY(onAdClosedEvent, RCTDirectEventBlock)