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.
- package/Buzzvil.podspec +26 -0
- package/LICENSE +20 -0
- package/README.md +58 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/buzzvil/BuzzvilModule.kt +90 -0
- package/android/src/main/java/com/buzzvil/BuzzvilNativeAdView.kt +250 -0
- package/android/src/main/java/com/buzzvil/BuzzvilNativeAdViewManager.kt +62 -0
- package/android/src/main/java/com/buzzvil/BuzzvilPackage.kt +37 -0
- package/android/src/main/res/layout/buzzvil_native_ad_banner.xml +68 -0
- package/android/src/main/res/layout/buzzvil_native_ad_card.xml +39 -0
- package/ios/Buzzvil.h +5 -0
- package/ios/Buzzvil.mm +100 -0
- package/ios/BuzzvilNativeAdView.h +14 -0
- package/ios/BuzzvilNativeAdView.mm +423 -0
- package/lib/module/BuzzvilNativeAdView.js +16 -0
- package/lib/module/BuzzvilNativeAdView.js.map +1 -0
- package/lib/module/BuzzvilNativeAdView.native.js +42 -0
- package/lib/module/BuzzvilNativeAdView.native.js.map +1 -0
- package/lib/module/BuzzvilNativeAdViewNativeComponent.ts +22 -0
- package/lib/module/NativeBuzzvil.js +32 -0
- package/lib/module/NativeBuzzvil.js.map +1 -0
- package/lib/module/buzzvil.js +25 -0
- package/lib/module/buzzvil.js.map +1 -0
- package/lib/module/buzzvil.native.js +64 -0
- package/lib/module/buzzvil.native.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/layout.js +39 -0
- package/lib/module/layout.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BuzzvilNativeAdView.d.ts +3 -0
- package/lib/typescript/src/BuzzvilNativeAdView.d.ts.map +1 -0
- package/lib/typescript/src/BuzzvilNativeAdView.native.d.ts +196 -0
- package/lib/typescript/src/BuzzvilNativeAdView.native.d.ts.map +1 -0
- package/lib/typescript/src/BuzzvilNativeAdViewNativeComponent.d.ts +25 -0
- package/lib/typescript/src/BuzzvilNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeBuzzvil.d.ts +72 -0
- package/lib/typescript/src/NativeBuzzvil.d.ts.map +1 -0
- package/lib/typescript/src/buzzvil.d.ts +7 -0
- package/lib/typescript/src/buzzvil.d.ts.map +1 -0
- package/lib/typescript/src/buzzvil.native.d.ts +25 -0
- package/lib/typescript/src/buzzvil.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/layout.d.ts +6 -0
- package/lib/typescript/src/layout.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +44 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +159 -0
- package/src/BuzzvilNativeAdView.native.tsx +48 -0
- package/src/BuzzvilNativeAdView.tsx +12 -0
- package/src/BuzzvilNativeAdViewNativeComponent.ts +22 -0
- package/src/NativeBuzzvil.ts +76 -0
- package/src/buzzvil.native.tsx +65 -0
- package/src/buzzvil.tsx +29 -0
- package/src/index.tsx +10 -0
- package/src/layout.ts +21 -0
- 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
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":[]}
|