react-native-buzzvil-ad 0.1.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.
Files changed (62) hide show
  1. package/Buzzvil.podspec +26 -0
  2. package/LICENSE +20 -0
  3. package/README.md +58 -0
  4. package/android/build.gradle +76 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/buzzvil/BuzzvilModule.kt +90 -0
  7. package/android/src/main/java/com/buzzvil/BuzzvilNativeAdView.kt +250 -0
  8. package/android/src/main/java/com/buzzvil/BuzzvilNativeAdViewManager.kt +62 -0
  9. package/android/src/main/java/com/buzzvil/BuzzvilPackage.kt +37 -0
  10. package/android/src/main/res/layout/buzzvil_native_ad_banner.xml +68 -0
  11. package/android/src/main/res/layout/buzzvil_native_ad_card.xml +39 -0
  12. package/ios/Buzzvil.h +5 -0
  13. package/ios/Buzzvil.mm +100 -0
  14. package/ios/BuzzvilNativeAdView.h +14 -0
  15. package/ios/BuzzvilNativeAdView.mm +423 -0
  16. package/lib/module/BuzzvilNativeAdView.js +16 -0
  17. package/lib/module/BuzzvilNativeAdView.js.map +1 -0
  18. package/lib/module/BuzzvilNativeAdView.native.js +42 -0
  19. package/lib/module/BuzzvilNativeAdView.native.js.map +1 -0
  20. package/lib/module/BuzzvilNativeAdViewNativeComponent.ts +22 -0
  21. package/lib/module/NativeBuzzvil.js +32 -0
  22. package/lib/module/NativeBuzzvil.js.map +1 -0
  23. package/lib/module/buzzvil.js +25 -0
  24. package/lib/module/buzzvil.js.map +1 -0
  25. package/lib/module/buzzvil.native.js +64 -0
  26. package/lib/module/buzzvil.native.js.map +1 -0
  27. package/lib/module/index.js +5 -0
  28. package/lib/module/index.js.map +1 -0
  29. package/lib/module/layout.js +39 -0
  30. package/lib/module/layout.js.map +1 -0
  31. package/lib/module/package.json +1 -0
  32. package/lib/module/types.js +2 -0
  33. package/lib/module/types.js.map +1 -0
  34. package/lib/typescript/package.json +1 -0
  35. package/lib/typescript/src/BuzzvilNativeAdView.d.ts +3 -0
  36. package/lib/typescript/src/BuzzvilNativeAdView.d.ts.map +1 -0
  37. package/lib/typescript/src/BuzzvilNativeAdView.native.d.ts +196 -0
  38. package/lib/typescript/src/BuzzvilNativeAdView.native.d.ts.map +1 -0
  39. package/lib/typescript/src/BuzzvilNativeAdViewNativeComponent.d.ts +25 -0
  40. package/lib/typescript/src/BuzzvilNativeAdViewNativeComponent.d.ts.map +1 -0
  41. package/lib/typescript/src/NativeBuzzvil.d.ts +72 -0
  42. package/lib/typescript/src/NativeBuzzvil.d.ts.map +1 -0
  43. package/lib/typescript/src/buzzvil.d.ts +7 -0
  44. package/lib/typescript/src/buzzvil.d.ts.map +1 -0
  45. package/lib/typescript/src/buzzvil.native.d.ts +25 -0
  46. package/lib/typescript/src/buzzvil.native.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +5 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -0
  49. package/lib/typescript/src/layout.d.ts +6 -0
  50. package/lib/typescript/src/layout.d.ts.map +1 -0
  51. package/lib/typescript/src/types.d.ts +44 -0
  52. package/lib/typescript/src/types.d.ts.map +1 -0
  53. package/package.json +159 -0
  54. package/src/BuzzvilNativeAdView.native.tsx +48 -0
  55. package/src/BuzzvilNativeAdView.tsx +12 -0
  56. package/src/BuzzvilNativeAdViewNativeComponent.ts +22 -0
  57. package/src/NativeBuzzvil.ts +76 -0
  58. package/src/buzzvil.native.tsx +65 -0
  59. package/src/buzzvil.tsx +29 -0
  60. package/src/index.tsx +10 -0
  61. package/src/layout.ts +21 -0
  62. package/src/types.ts +45 -0
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ Native (banner) ad layout for the 320x50 / 320x100 / 320x130 inventory sizes.
4
+ Horizontal, compact: icon + title/description + CTA, with NO visible media.
5
+
6
+ IDs MUST match findViewById() in BuzzvilNativeAdView.kt and the
7
+ BuzzNativeViewBinder.Builder wiring. The root BuzzNativeAdView is a FrameLayout
8
+ subclass, so the actual horizontal arrangement lives in the inner LinearLayout.
9
+
10
+ buzz_media is REQUIRED: BuzzNativeViewBinder.Builder.build() throws
11
+ "MediaView is not bound." if buzzMediaView() is null (verified via javap on
12
+ buzzad-benefit-base 6.7.7). It is kept 0dp + gone so no media is shown for
13
+ banners while still satisfying the binder.
14
+ -->
15
+ <com.buzzvil.buzzbenefit.buzznative.BuzzNativeAdView
16
+ xmlns:android="http://schemas.android.com/apk/res/android"
17
+ android:id="@+id/buzz_ad_view"
18
+ android:layout_width="match_parent"
19
+ android:layout_height="match_parent">
20
+
21
+ <com.buzzvil.buzzbenefit.buzznative.BuzzMediaView
22
+ android:id="@+id/buzz_media"
23
+ android:layout_width="0dp"
24
+ android:layout_height="0dp"
25
+ android:visibility="gone" />
26
+
27
+ <LinearLayout
28
+ android:layout_width="match_parent"
29
+ android:layout_height="match_parent"
30
+ android:orientation="horizontal"
31
+ android:gravity="center_vertical"
32
+ android:paddingStart="8dp"
33
+ android:paddingEnd="8dp">
34
+
35
+ <ImageView
36
+ android:id="@+id/buzz_icon"
37
+ android:layout_width="36dp"
38
+ android:layout_height="36dp"
39
+ android:layout_marginEnd="8dp" />
40
+
41
+ <LinearLayout
42
+ android:layout_width="0dp"
43
+ android:layout_height="wrap_content"
44
+ android:layout_weight="1"
45
+ android:orientation="vertical">
46
+
47
+ <TextView
48
+ android:id="@+id/buzz_title"
49
+ android:layout_width="match_parent"
50
+ android:layout_height="wrap_content"
51
+ android:maxLines="1"
52
+ android:ellipsize="end" />
53
+
54
+ <TextView
55
+ android:id="@+id/buzz_desc"
56
+ android:layout_width="match_parent"
57
+ android:layout_height="wrap_content"
58
+ android:maxLines="1"
59
+ android:ellipsize="end" />
60
+ </LinearLayout>
61
+
62
+ <com.buzzvil.buzzbenefit.DefaultBuzzCtaView
63
+ android:id="@+id/buzz_cta"
64
+ android:layout_width="wrap_content"
65
+ android:layout_height="wrap_content"
66
+ android:layout_marginStart="8dp" />
67
+ </LinearLayout>
68
+ </com.buzzvil.buzzbenefit.buzznative.BuzzNativeAdView>
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ Native (in-feed) ad card. IDs MUST match findViewById() in BuzzvilNativeAdView.kt
4
+ and the BuzzNativeViewBinder.Builder wiring. The root BuzzNativeAdView is a
5
+ FrameLayout subclass, so android:orientation is ignored — the real visual
6
+ layout (spacing/stacking) is refined in a later task; only the binder view
7
+ references matter here.
8
+ -->
9
+ <com.buzzvil.buzzbenefit.buzznative.BuzzNativeAdView
10
+ xmlns:android="http://schemas.android.com/apk/res/android"
11
+ android:id="@+id/buzz_ad_view"
12
+ android:layout_width="match_parent"
13
+ android:layout_height="wrap_content">
14
+
15
+ <com.buzzvil.buzzbenefit.buzznative.BuzzMediaView
16
+ android:id="@+id/buzz_media"
17
+ android:layout_width="match_parent"
18
+ android:layout_height="wrap_content" />
19
+
20
+ <ImageView
21
+ android:id="@+id/buzz_icon"
22
+ android:layout_width="36dp"
23
+ android:layout_height="36dp" />
24
+
25
+ <TextView
26
+ android:id="@+id/buzz_title"
27
+ android:layout_width="wrap_content"
28
+ android:layout_height="wrap_content" />
29
+
30
+ <TextView
31
+ android:id="@+id/buzz_desc"
32
+ android:layout_width="wrap_content"
33
+ android:layout_height="wrap_content" />
34
+
35
+ <com.buzzvil.buzzbenefit.DefaultBuzzCtaView
36
+ android:id="@+id/buzz_cta"
37
+ android:layout_width="wrap_content"
38
+ android:layout_height="wrap_content" />
39
+ </com.buzzvil.buzzbenefit.buzznative.BuzzNativeAdView>
package/ios/Buzzvil.h ADDED
@@ -0,0 +1,5 @@
1
+ #import <BuzzvilSpec/BuzzvilSpec.h>
2
+
3
+ @interface Buzzvil : NSObject <NativeBuzzvilSpec>
4
+
5
+ @end
package/ios/Buzzvil.mm ADDED
@@ -0,0 +1,100 @@
1
+ #import "Buzzvil.h"
2
+
3
+ #import <React/RCTUtils.h>
4
+
5
+ // BuzzvilSDK exposes the high-level `BuzzBenefit` session API; BuzzAdBenefitSDK
6
+ // exposes `BuzzBenefitHub`. Both are @objc and verified against the installed
7
+ // frameworks' generated `-Swift.h` headers (BuzzvilSDK 6.7.5).
8
+ #import <BuzzvilSDK/BuzzvilSDK-Swift.h>
9
+ #import <BuzzAdBenefitSDK/BuzzAdBenefitSDK-Swift.h>
10
+
11
+ @implementation Buzzvil
12
+
13
+ - (void)initialize:(NSString *)appId
14
+ {
15
+ BuzzBenefitConfig *config = [BuzzBenefitConfig configWith:^(BuzzBenefitConfigBuilder *builder) {
16
+ builder.appId = appId;
17
+ }];
18
+ [[BuzzBenefit sharedInstance] initializeWith:config onCompleted:nil];
19
+ }
20
+
21
+ - (void)login:(NSString *)userId
22
+ gender:(NSString *)gender
23
+ birthYear:(double)birthYear
24
+ resolve:(RCTPromiseResolveBlock)resolve
25
+ reject:(RCTPromiseRejectBlock)reject
26
+ {
27
+ BuzzBenefitUser *user = [BuzzBenefitUser userWith:^(BuzzBenefitUserBuilder *builder) {
28
+ builder.userId = userId;
29
+ // Sentinel contract (see NativeBuzzvil.ts): "" → leave gender unset.
30
+ if ([gender isEqualToString:@"MALE"]) {
31
+ builder.gender = BuzzBenefitUserGenderMale;
32
+ } else if ([gender isEqualToString:@"FEMALE"]) {
33
+ builder.gender = BuzzBenefitUserGenderFemale;
34
+ }
35
+ // Sentinel: 0 → leave birth year unset.
36
+ if (birthYear > 0) {
37
+ builder.birthYear = (NSInteger)birthYear;
38
+ }
39
+ }];
40
+
41
+ [[BuzzBenefit sharedInstance] loginWith:user
42
+ onSuccess:^{
43
+ resolve(nil);
44
+ }
45
+ onFailure:^(NSError *error) {
46
+ reject(@"buzzvil_login_failed",
47
+ error.localizedDescription ?: @"Buzzvil login failed",
48
+ error);
49
+ }];
50
+ }
51
+
52
+ - (void)logout
53
+ {
54
+ [[BuzzBenefit sharedInstance] logout];
55
+ }
56
+
57
+ - (void)isLoggedIn:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
58
+ {
59
+ resolve(@([[BuzzBenefit sharedInstance] isLoggedIn]));
60
+ }
61
+
62
+ - (void)showBenefitHub:(NSString *)routePath showHistory:(BOOL)showHistory
63
+ {
64
+ // BenefitHub presentation is UI work — must run on the main thread.
65
+ dispatch_async(dispatch_get_main_queue(), ^{
66
+ UIViewController *presenter = RCTPresentedViewController();
67
+ if (presenter == nil) {
68
+ return;
69
+ }
70
+
71
+ BuzzBenefitHub *hub = [BuzzBenefitHub new];
72
+
73
+ if (routePath.length > 0 || showHistory) {
74
+ BuzzBenefitHubConfig *config = [BuzzBenefitHubConfig configWith:^(BuzzBenefitHubConfigBuilder *builder) {
75
+ if (routePath.length > 0) {
76
+ builder.routePath = routePath;
77
+ }
78
+ if (showHistory) {
79
+ builder.queryParams = [[BuzzBenefitHubPage history] toRedirectQueryParams];
80
+ }
81
+ }];
82
+ [hub setConfig:config];
83
+ }
84
+
85
+ [hub showOn:presenter];
86
+ });
87
+ }
88
+
89
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
90
+ (const facebook::react::ObjCTurboModule::InitParams &)params
91
+ {
92
+ return std::make_shared<facebook::react::NativeBuzzvilSpecJSI>(params);
93
+ }
94
+
95
+ + (NSString *)moduleName
96
+ {
97
+ return @"Buzzvil";
98
+ }
99
+
100
+ @end
@@ -0,0 +1,14 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #ifndef BuzzvilNativeAdViewNativeComponent_h
5
+ #define BuzzvilNativeAdViewNativeComponent_h
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ @interface BuzzvilNativeAdView : RCTViewComponentView
10
+ @end
11
+
12
+ NS_ASSUME_NONNULL_END
13
+
14
+ #endif /* BuzzvilNativeAdViewNativeComponent_h */
@@ -0,0 +1,423 @@
1
+ #import "BuzzvilNativeAdView.h"
2
+
3
+ // BuzzAdBenefitSDK exposes the BuzzNative loader/binder/view classes as @objc;
4
+ // verified against the installed framework's generated -Swift.h (BuzzAdBenefitSDK
5
+ // 6.7.5). Same direct-from-Obj-C++ import the TurboModule (`Buzzvil.mm`) uses.
6
+ #import <BuzzAdBenefitSDK/BuzzAdBenefitSDK-Swift.h>
7
+
8
+ #import <react/renderer/components/BuzzvilSpec/ComponentDescriptors.h>
9
+ #import <react/renderer/components/BuzzvilSpec/EventEmitters.h>
10
+ #import <react/renderer/components/BuzzvilSpec/Props.h>
11
+ #import <react/renderer/components/BuzzvilSpec/RCTComponentViewHelpers.h>
12
+
13
+ using namespace facebook::react;
14
+
15
+ // Private helper declared up-front so the load callback (defined before the
16
+ // method body) can reference it.
17
+ @interface BuzzvilNativeAdView ()
18
+ + (NSString *)symbolicCodeForError:(NSError *)error;
19
+ @end
20
+
21
+ @implementation BuzzvilNativeAdView {
22
+ // Buzzvil ad card subviews, held so the binder can wire them post-load.
23
+ BuzzNativeAdView *_adContainer;
24
+ BuzzMediaView *_mediaView;
25
+ UIImageView *_iconView;
26
+ UILabel *_titleLabel;
27
+ UILabel *_descriptionLabel;
28
+ BuzzDefaultCtaView *_ctaView;
29
+
30
+ BuzzNative *_native;
31
+ BuzzNativeViewBinder *_binder;
32
+
33
+ // Layout-variant state. The subviews are created in initWithFrame:, but the
34
+ // `layout` prop (which family + height to use) only arrives in updateProps:.
35
+ // Fabric recycles iOS views by COMPONENT TYPE, not by props, so a view first
36
+ // mounted as a banner can be reused for a card cell — the arrangement must be
37
+ // (re)applied whenever the resolved variant changes, not just once. We track
38
+ // the variant currently applied and the constraints we activated for it so a
39
+ // change can deactivate the old set before activating the new one.
40
+ NSString *_appliedVariant;
41
+ NSArray<NSLayoutConstraint *> *_variantConstraints;
42
+
43
+ // Set when a load succeeds; the real size is emitted from layoutSubviews once
44
+ // bounds are non-zero (a pre-layout emit would report {w,0}). Reset per load /
45
+ // on recycle so we emit exactly once per successful load.
46
+ BOOL _pendingLoadedEmit;
47
+
48
+ // The unit id of the in-flight / loaded ad. Doubles as the recycle guard:
49
+ // Fabric reuses the SAME object across cells (prepareForRecycle), so a stale
50
+ // async callback must bail when this no longer matches the id it captured.
51
+ // Cleared in prepareForRecycle — never latched permanently (that would brick
52
+ // the recycled view).
53
+ std::string _loadedUnitId;
54
+ }
55
+
56
+ + (ComponentDescriptorProvider)componentDescriptorProvider
57
+ {
58
+ return concreteComponentDescriptorProvider<BuzzvilNativeAdViewComponentDescriptor>();
59
+ }
60
+
61
+ - (instancetype)initWithFrame:(CGRect)frame
62
+ {
63
+ if (self = [super initWithFrame:frame]) {
64
+ static const auto defaultProps = std::make_shared<const BuzzvilNativeAdViewProps>();
65
+ _props = defaultProps;
66
+
67
+ [self buildSubviews];
68
+ self.contentView = _adContainer;
69
+ }
70
+
71
+ return self;
72
+ }
73
+
74
+ // Allocates the BuzzNativeAdView container and its subviews (media, icon, title,
75
+ // description, CTA) and adds them to the container. Does NOT activate any
76
+ // arrangement constraints — the family (banner vs card) is only known once the
77
+ // `layout` prop arrives, so layout is applied later in applyLayoutVariant:.
78
+ - (void)buildSubviews
79
+ {
80
+ _adContainer = [[BuzzNativeAdView alloc] initWithFrame:CGRectZero];
81
+
82
+ _mediaView = [[BuzzMediaView alloc] initWithFrame:CGRectZero];
83
+ _iconView = [[UIImageView alloc] initWithFrame:CGRectZero];
84
+ _iconView.contentMode = UIViewContentModeScaleAspectFit;
85
+ _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
86
+ _titleLabel.numberOfLines = 1;
87
+ _descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
88
+ _descriptionLabel.numberOfLines = 2;
89
+ _ctaView = [[BuzzDefaultCtaView alloc] initWithFrame:CGRectZero];
90
+
91
+ NSArray<UIView *> *children = @[ _mediaView, _iconView, _titleLabel, _descriptionLabel, _ctaView ];
92
+ for (UIView *child in children) {
93
+ child.translatesAutoresizingMaskIntoConstraints = NO;
94
+ [_adContainer addSubview:child];
95
+ }
96
+ }
97
+
98
+ // Fixed inventory-box height (pt) per exact size. Width comes from the JS
99
+ // `style`. NOTE: under Fabric the host frame is ultimately driven by the shadow
100
+ // node, so this height is a best-effort hint (see PR notes).
101
+ - (CGFloat)heightForVariant:(NSString *)variant
102
+ {
103
+ if ([variant isEqualToString:@"320x50"]) return 50;
104
+ if ([variant isEqualToString:@"320x100"]) return 100;
105
+ if ([variant isEqualToString:@"320x130"]) return 130;
106
+ if ([variant isEqualToString:@"320x480"]) return 480;
107
+ return 250; // 300x250 and default
108
+ }
109
+
110
+ - (BOOL)isBannerVariant:(NSString *)variant
111
+ {
112
+ return [variant isEqualToString:@"320x50"] || [variant isEqualToString:@"320x100"] ||
113
+ [variant isEqualToString:@"320x130"];
114
+ }
115
+
116
+ // Activates the correct arrangement (banner OR card) plus a fixed height
117
+ // constraint. Banner: horizontal icon + title/desc + CTA, media hidden+zero-size
118
+ // (the binder REQUIRES a non-nil mediaView — verified against the SDK header:
119
+ // "Required component"). Card: the original vertical stack with media on top.
120
+ // Re-appliable: a no-op when the variant is unchanged, otherwise the previously
121
+ // activated constraints are deactivated first (a recycled view may switch family).
122
+ - (void)applyLayoutVariant:(NSString *)variant
123
+ {
124
+ if ([variant isEqualToString:_appliedVariant]) {
125
+ return;
126
+ }
127
+ if (_variantConstraints.count > 0) {
128
+ [NSLayoutConstraint deactivateConstraints:_variantConstraints];
129
+ }
130
+ _appliedVariant = variant;
131
+
132
+ BOOL banner = [self isBannerVariant:variant];
133
+ _mediaView.hidden = banner;
134
+
135
+ NSMutableArray<NSLayoutConstraint *> *constraints = [NSMutableArray array];
136
+
137
+ if (banner) {
138
+ // Media kept in the tree (binder requires it) but collapsed to zero size.
139
+ [constraints addObjectsFromArray:@[
140
+ [_mediaView.widthAnchor constraintEqualToConstant:0],
141
+ [_mediaView.heightAnchor constraintEqualToConstant:0],
142
+ [_mediaView.topAnchor constraintEqualToAnchor:_adContainer.topAnchor],
143
+ [_mediaView.leadingAnchor constraintEqualToAnchor:_adContainer.leadingAnchor],
144
+
145
+ [_iconView.leadingAnchor constraintEqualToAnchor:_adContainer.leadingAnchor constant:8],
146
+ [_iconView.centerYAnchor constraintEqualToAnchor:_adContainer.centerYAnchor],
147
+ [_iconView.widthAnchor constraintEqualToConstant:36],
148
+ [_iconView.heightAnchor constraintEqualToConstant:36],
149
+
150
+ [_titleLabel.topAnchor constraintEqualToAnchor:_adContainer.topAnchor constant:8],
151
+ [_titleLabel.leadingAnchor constraintEqualToAnchor:_iconView.trailingAnchor constant:8],
152
+ [_titleLabel.trailingAnchor constraintEqualToAnchor:_ctaView.leadingAnchor constant:-8],
153
+
154
+ [_descriptionLabel.topAnchor constraintEqualToAnchor:_titleLabel.bottomAnchor constant:2],
155
+ [_descriptionLabel.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor],
156
+ [_descriptionLabel.trailingAnchor constraintEqualToAnchor:_titleLabel.trailingAnchor],
157
+
158
+ [_ctaView.trailingAnchor constraintEqualToAnchor:_adContainer.trailingAnchor constant:-8],
159
+ [_ctaView.centerYAnchor constraintEqualToAnchor:_adContainer.centerYAnchor],
160
+ ]];
161
+ } else {
162
+ // Vertical stack: media on top, then icon + title/desc, then CTA.
163
+ [constraints addObjectsFromArray:@[
164
+ [_mediaView.topAnchor constraintEqualToAnchor:_adContainer.topAnchor],
165
+ [_mediaView.leadingAnchor constraintEqualToAnchor:_adContainer.leadingAnchor],
166
+ [_mediaView.trailingAnchor constraintEqualToAnchor:_adContainer.trailingAnchor],
167
+
168
+ [_iconView.topAnchor constraintEqualToAnchor:_mediaView.bottomAnchor constant:8],
169
+ [_iconView.leadingAnchor constraintEqualToAnchor:_adContainer.leadingAnchor constant:8],
170
+ [_iconView.widthAnchor constraintEqualToConstant:40],
171
+ [_iconView.heightAnchor constraintEqualToConstant:40],
172
+
173
+ [_titleLabel.topAnchor constraintEqualToAnchor:_iconView.topAnchor],
174
+ [_titleLabel.leadingAnchor constraintEqualToAnchor:_iconView.trailingAnchor constant:8],
175
+ [_titleLabel.trailingAnchor constraintEqualToAnchor:_adContainer.trailingAnchor constant:-8],
176
+
177
+ [_descriptionLabel.topAnchor constraintEqualToAnchor:_titleLabel.bottomAnchor constant:4],
178
+ [_descriptionLabel.leadingAnchor constraintEqualToAnchor:_titleLabel.leadingAnchor],
179
+ [_descriptionLabel.trailingAnchor constraintEqualToAnchor:_adContainer.trailingAnchor constant:-8],
180
+
181
+ [_ctaView.topAnchor constraintEqualToAnchor:_iconView.bottomAnchor constant:8],
182
+ [_ctaView.leadingAnchor constraintEqualToAnchor:_adContainer.leadingAnchor constant:8],
183
+ [_ctaView.trailingAnchor constraintEqualToAnchor:_adContainer.trailingAnchor constant:-8],
184
+ [_ctaView.bottomAnchor constraintEqualToAnchor:_adContainer.bottomAnchor constant:-8],
185
+ ]];
186
+ }
187
+
188
+ // Fixed inventory-box height on the container.
189
+ [constraints addObject:[_adContainer.heightAnchor constraintEqualToConstant:[self heightForVariant:variant]]];
190
+
191
+ _variantConstraints = constraints;
192
+ [NSLayoutConstraint activateConstraints:constraints];
193
+ }
194
+
195
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
196
+ {
197
+ const auto &newViewProps = *std::static_pointer_cast<BuzzvilNativeAdViewProps const>(props);
198
+
199
+ // Resolve the layout family/height from the `layout` prop (empty -> card
200
+ // default) and apply the arrangement. applyLayoutVariant: is a no-op when the
201
+ // variant is unchanged, so this runs on every updateProps but only does work
202
+ // on first mount or when a recycled view switches family. Must happen before
203
+ // the load below so the binder sees the correct (banner vs card) view tree.
204
+ NSString *variant = newViewProps.layout.empty()
205
+ ? @"300x250"
206
+ : [NSString stringWithUTF8String:newViewProps.layout.c_str()];
207
+ [self applyLayoutVariant:variant];
208
+
209
+ const std::string &unitId = newViewProps.unitId;
210
+ // Fabric sets props in any order and may re-deliver them; only (re)load when a
211
+ // non-empty unit id actually changes.
212
+ if (!unitId.empty() && unitId != _loadedUnitId) {
213
+ _loadedUnitId = unitId;
214
+ [self loadAdWithUnitId:unitId];
215
+ }
216
+
217
+ [super updateProps:props oldProps:oldProps];
218
+ }
219
+
220
+ - (void)loadAdWithUnitId:(const std::string &)unitId
221
+ {
222
+ // Tear down any previous ad before a new load (mirrors prepareForRecycle);
223
+ // a unitId prop change on a mounted view reuses this same object.
224
+ [_binder unbind];
225
+ _binder = nil;
226
+ _native = nil;
227
+ _pendingLoadedEmit = NO;
228
+
229
+ NSString *unitIdString = [NSString stringWithUTF8String:unitId.c_str()];
230
+ // Capture the id this load is for; the recycle guard compares against it.
231
+ std::string requestedUnitId = unitId;
232
+
233
+ _native = [[BuzzNative alloc] initWithUnitId:unitIdString];
234
+
235
+ // Subscribe BEFORE load (the SDK header mandates this ordering). Weak self in
236
+ // every block: `_native` is a strong ivar that retains these blocks, so strong
237
+ // capture would form a self -> _native -> block -> self cycle.
238
+ // All emits hop to main: the `_loadedUnitId` recycle guard is written on the
239
+ // main thread (updateProps / prepareForRecycle), so its callback reads must be
240
+ // on main too, and it keeps the threading model uniform with the load path.
241
+ __weak BuzzvilNativeAdView *weakSelf = self;
242
+ [_native subscribeAdEventsOnImpressed:^(BuzzNativeAd *ad) {
243
+ dispatch_async(dispatch_get_main_queue(), ^{
244
+ [weakSelf emitImpressedForUnitId:requestedUnitId];
245
+ });
246
+ }
247
+ onClicked:^(BuzzNativeAd *ad) {
248
+ dispatch_async(dispatch_get_main_queue(), ^{
249
+ [weakSelf emitClickedForUnitId:requestedUnitId];
250
+ });
251
+ }
252
+ onRewardRequested:^(BuzzNativeAd *ad) {
253
+ // No matching JS event.
254
+ }
255
+ onRewarded:^(BuzzNativeAd *ad, enum BuzzRewardResult result) {
256
+ BOOL success = (result == BuzzRewardResultSuccess);
257
+ dispatch_async(dispatch_get_main_queue(), ^{
258
+ [weakSelf emitRewarded:success forUnitId:requestedUnitId];
259
+ });
260
+ }
261
+ onParticipated:^(BuzzNativeAd *ad) {
262
+ // No matching JS event.
263
+ }];
264
+
265
+ [_native loadOnSuccess:^(BuzzNativeAd *ad) {
266
+ // Binding + emitting touches UIKit and the event emitter — hop to main and
267
+ // re-check the recycle guard AFTER the hop, since a recycle can land during it.
268
+ dispatch_async(dispatch_get_main_queue(), ^{
269
+ BuzzvilNativeAdView *strongSelf = weakSelf;
270
+ if (strongSelf == nil || strongSelf->_loadedUnitId != requestedUnitId) {
271
+ return;
272
+ }
273
+ [strongSelf bindLoadedAd];
274
+ });
275
+ }
276
+ onFailure:^(NSError *error) {
277
+ // Converge on the SAME symbolic UPPER_SNAKE code Android emits via
278
+ // `BuzzAdError.Type.name`, so a JS consumer can branch on `code`
279
+ // portably. The SDK's NSError carries a `BuzzErrorCode` raw value
280
+ // (domain `com.buzzvil.sdk.error`); map each to Android's name. Note
281
+ // code 6 maps to GOOGLE_AGE_POLICY to match Android's actual enum name
282
+ // (Android's `Type` is GOOGLE_AGE_POLICY, not AGE_POLICY).
283
+ NSString *codeString = [BuzzvilNativeAdView symbolicCodeForError:error];
284
+ NSString *messageString = error.localizedDescription;
285
+ dispatch_async(dispatch_get_main_queue(), ^{
286
+ BuzzvilNativeAdView *strongSelf = weakSelf;
287
+ if (strongSelf == nil || strongSelf->_loadedUnitId != requestedUnitId) {
288
+ return;
289
+ }
290
+ [strongSelf emitFailedWithCode:codeString message:messageString];
291
+ });
292
+ }];
293
+ }
294
+
295
+ - (void)bindLoadedAd
296
+ {
297
+ _binder = [BuzzNativeViewBinder viewBinderWith:^(BuzzNativeViewBinderBuilder *builder) {
298
+ builder.nativeAdView = _adContainer;
299
+ builder.mediaView = _mediaView;
300
+ builder.iconImageView = _iconView;
301
+ builder.titleLabel = _titleLabel;
302
+ builder.descriptionLabel = _descriptionLabel;
303
+ builder.ctaView = _ctaView;
304
+ }];
305
+ // bind() takes the BuzzNative loader, not the loaded BuzzNativeAd (mirrors Android).
306
+ [_binder bind:_native];
307
+
308
+ // Defer the size emit to layoutSubviews: at bind time the view hasn't been
309
+ // laid out yet, so bounds would be {w,0}. layoutSubviews fires once a real
310
+ // frame is assigned; the flag makes us emit exactly once per load.
311
+ _pendingLoadedEmit = YES;
312
+ [self setNeedsLayout];
313
+ }
314
+
315
+ - (void)layoutSubviews
316
+ {
317
+ [super layoutSubviews];
318
+
319
+ // Emit the real measured size once the host has a non-zero frame.
320
+ if (_pendingLoadedEmit && self.bounds.size.width > 0 && self.bounds.size.height > 0) {
321
+ _pendingLoadedEmit = NO;
322
+ CGSize size = self.bounds.size;
323
+ [self emitLoadedWithWidth:size.width height:size.height];
324
+ }
325
+ }
326
+
327
+ #pragma mark - Event emitter
328
+
329
+ - (const BuzzvilNativeAdViewEventEmitter &)eventEmitterRef
330
+ {
331
+ return static_cast<const BuzzvilNativeAdViewEventEmitter &>(*_eventEmitter);
332
+ }
333
+
334
+ - (void)emitLoadedWithWidth:(CGFloat)width height:(CGFloat)height
335
+ {
336
+ if (!_eventEmitter) {
337
+ return;
338
+ }
339
+ [self eventEmitterRef].onAdLoaded(
340
+ BuzzvilNativeAdViewEventEmitter::OnAdLoaded{.width = (double)width, .height = (double)height});
341
+ }
342
+
343
+ // Maps the SDK's NSError (a `BuzzErrorCode` raw value under domain
344
+ // `com.buzzvil.sdk.error`) to the SAME symbolic UPPER_SNAKE string Android
345
+ // emits via `BuzzAdError.Type.name`, so `code` is portable across platforms.
346
+ // Defaults to "UNKNOWN" for any unrecognized code (and for non-Buzzvil errors).
347
+ + (NSString *)symbolicCodeForError:(NSError *)error
348
+ {
349
+ switch ((enum BuzzErrorCode)error.code) {
350
+ case BuzzErrorCodeServerError:
351
+ return @"SERVER_ERROR";
352
+ case BuzzErrorCodeClientError:
353
+ return @"CLIENT_ERROR";
354
+ case BuzzErrorCodeConnectionTimeout:
355
+ return @"CONNECTION_TIMEOUT";
356
+ case BuzzErrorCodeEmptyResponse:
357
+ return @"EMPTY_RESPONSE";
358
+ case BuzzErrorCodeWaitingForResponse:
359
+ return @"WAITING_FOR_RESPONSE";
360
+ case BuzzErrorCodeAgePolicy:
361
+ // Android's enum name is GOOGLE_AGE_POLICY; match it for a shared string.
362
+ return @"GOOGLE_AGE_POLICY";
363
+ case BuzzErrorCodePrivacyPolicyNotGranted:
364
+ return @"PRIVACY_POLICY_NOT_GRANTED";
365
+ case BuzzErrorCodeNotInitialized:
366
+ return @"NOT_INITIALIZED";
367
+ case BuzzErrorCodeUnknown:
368
+ default:
369
+ return @"UNKNOWN";
370
+ }
371
+ }
372
+
373
+ - (void)emitFailedWithCode:(NSString *)code message:(NSString *)message
374
+ {
375
+ if (!_eventEmitter) {
376
+ return;
377
+ }
378
+ [self eventEmitterRef].onAdFailed(BuzzvilNativeAdViewEventEmitter::OnAdFailed{
379
+ .code = std::string(code.UTF8String ?: ""),
380
+ .message = std::string(message.UTF8String ?: "")});
381
+ }
382
+
383
+ - (void)emitClickedForUnitId:(const std::string &)unitId
384
+ {
385
+ if (!_eventEmitter || _loadedUnitId != unitId) {
386
+ return;
387
+ }
388
+ [self eventEmitterRef].onAdClicked(BuzzvilNativeAdViewEventEmitter::OnAdClicked{});
389
+ }
390
+
391
+ - (void)emitImpressedForUnitId:(const std::string &)unitId
392
+ {
393
+ if (!_eventEmitter || _loadedUnitId != unitId) {
394
+ return;
395
+ }
396
+ [self eventEmitterRef].onImpressed(BuzzvilNativeAdViewEventEmitter::OnImpressed{});
397
+ }
398
+
399
+ - (void)emitRewarded:(BOOL)success forUnitId:(const std::string &)unitId
400
+ {
401
+ if (!_eventEmitter || _loadedUnitId != unitId) {
402
+ return;
403
+ }
404
+ [self eventEmitterRef].onRewarded(BuzzvilNativeAdViewEventEmitter::OnRewarded{.success = (bool)success});
405
+ }
406
+
407
+ #pragma mark - Recycling
408
+
409
+ - (void)prepareForRecycle
410
+ {
411
+ // Clearing _loadedUnitId is the recycle guard: any in-flight callback captured
412
+ // the old id and will bail on mismatch. Do NOT latch a permanent disposed flag
413
+ // — Fabric reuses this same object, so it must be able to load again.
414
+ [_binder unbind];
415
+ _binder = nil;
416
+ _native = nil;
417
+ _loadedUnitId.clear();
418
+ _pendingLoadedEmit = NO;
419
+
420
+ [super prepareForRecycle];
421
+ }
422
+
423
+ @end
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ import { View } from 'react-native';
4
+ import { sizeForLayout } from "./layout.js";
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ export function BuzzvilNativeAdView({
7
+ layout,
8
+ style
9
+ }) {
10
+ // Buzzvil native ads are not available on web; render nothing visible, but
11
+ // reserve the same box as native (consumer `style` overrides the default).
12
+ return /*#__PURE__*/_jsx(View, {
13
+ style: [sizeForLayout(layout), style]
14
+ });
15
+ }
16
+ //# sourceMappingURL=BuzzvilNativeAdView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["View","sizeForLayout","jsx","_jsx","BuzzvilNativeAdView","layout","style"],"sourceRoot":"../../src","sources":["BuzzvilNativeAdView.tsx"],"mappings":";;AAAA,SAASA,IAAI,QAAQ,cAAc;AACnC,SAASC,aAAa,QAAQ,aAAU;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGzC,OAAO,SAASC,mBAAmBA,CAAC;EAClCC,MAAM;EACNC;AACwB,CAAC,EAAE;EAC3B;EACA;EACA,oBAAOH,IAAA,CAACH,IAAI;IAACM,KAAK,EAAE,CAACL,aAAa,CAACI,MAAM,CAAC,EAAEC,KAAK;EAAE,CAAE,CAAC;AACxD","ignoreList":[]}