@thealteroffice/react-native-adgeist 0.0.15 → 0.0.17
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/android/build.gradle +1 -1
- package/android/generated/java/com/adgeist/NativeAdgeistSpec.java +83 -0
- package/android/generated/jni/CMakeLists.txt +36 -0
- package/android/generated/jni/RNAdgeistSpec-generated.cpp +98 -0
- package/android/generated/jni/RNAdgeistSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNAdgeistSpec/RNAdgeistSpecJSI-generated.cpp +149 -0
- package/android/generated/jni/react/renderer/components/RNAdgeistSpec/RNAdgeistSpecJSI.h +170 -0
- package/android/src/main/java/com/adgeist/implementation/AdgeistModuleImpl.kt +101 -9
- package/android/src/newarch/java/com/AdgeistModule.kt +93 -2
- package/android/src/oldarch/java/com/AdgeistModule.kt +101 -4
- package/ios/generated/RNAdgeistSpec/RNAdgeistSpec-generated.mm +42 -7
- package/ios/generated/RNAdgeistSpec/RNAdgeistSpec.h +57 -10
- package/ios/generated/RNAdgeistSpecJSI-generated.cpp +81 -14
- package/ios/generated/RNAdgeistSpecJSI.h +54 -9
- package/lib/module/NativeAdgeist.js.map +1 -1
- package/lib/module/components/BannerAd.js +164 -46
- package/lib/module/components/BannerAd.js.map +1 -1
- package/lib/module/utilities.js +16 -0
- package/lib/module/utilities.js.map +1 -0
- package/lib/typescript/src/NativeAdgeist.d.ts +6 -1
- package/lib/typescript/src/NativeAdgeist.d.ts.map +1 -1
- package/lib/typescript/src/components/BannerAd.d.ts.map +1 -1
- package/lib/typescript/src/utilities.d.ts +5 -0
- package/lib/typescript/src/utilities.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/NativeAdgeist.ts +61 -9
- package/src/components/BannerAd.tsx +226 -63
- package/src/utilities.ts +13 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @description A React Native component for displaying banner and video ads with robust error handling and analytics.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
|
7
7
|
import {
|
|
8
8
|
Image,
|
|
9
9
|
Linking,
|
|
@@ -12,10 +12,12 @@ import {
|
|
|
12
12
|
View,
|
|
13
13
|
TouchableWithoutFeedback,
|
|
14
14
|
ActivityIndicator,
|
|
15
|
+
Dimensions,
|
|
15
16
|
} from 'react-native';
|
|
16
17
|
import Video from 'react-native-video';
|
|
17
18
|
import Adgeist from '../NativeAdgeist';
|
|
18
19
|
import { useAdgeistContext } from './AdgeistProvider';
|
|
20
|
+
import { normalizeUrl } from '../utilities';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Interface for ad data structure
|
|
@@ -84,22 +86,36 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
84
86
|
width = 0,
|
|
85
87
|
height = 0,
|
|
86
88
|
isResponsive = false,
|
|
87
|
-
// responsiveType = 'SQUARE',
|
|
88
89
|
onAdLoadError,
|
|
89
90
|
onAdLoadSuccess,
|
|
90
91
|
}) => {
|
|
91
92
|
const [adData, setAdData] = useState<AdData | null>(null);
|
|
92
|
-
const [isMuted, setIsMuted] = useState<boolean>(
|
|
93
|
+
const [isMuted, setIsMuted] = useState<boolean>(true);
|
|
93
94
|
const [error, setError] = useState<Error | null>(null);
|
|
94
95
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
96
|
+
const [hasImpression, setHasImpression] = useState<boolean>(false);
|
|
97
|
+
const [hasView, setHasView] = useState<boolean>(false);
|
|
98
|
+
const renderStartTime = useRef(Date.now());
|
|
99
|
+
const adRef = useRef<View>(null);
|
|
100
|
+
const visibilityStartTime = useRef<number | null>(null);
|
|
101
|
+
const timeToVisible = useRef<number | null>(null);
|
|
102
|
+
const viewTime = useRef<number>(0);
|
|
103
|
+
const lastCheckTime = useRef<number>(Date.now());
|
|
104
|
+
const currentVisibilityRatio = useRef<number>(0);
|
|
105
|
+
const videoRef = useRef<any>(null);
|
|
106
|
+
const lastPausedTime = useRef<number>(0);
|
|
107
|
+
const isInView = useRef<boolean>(false);
|
|
95
108
|
|
|
96
109
|
const { isInitialized, publisherId, apiKey, domain, isTestEnvironment } =
|
|
97
110
|
useAdgeistContext();
|
|
98
111
|
|
|
99
112
|
const creativeData = adData?.seatBid?.[0]?.bid?.[0]?.ext as BidExtension;
|
|
113
|
+
const bidId = adData?.id;
|
|
114
|
+
const campaignId = adData?.seatBid?.[0]?.bid?.[0]?.id;
|
|
115
|
+
const adSpaceId = dataAdSlot;
|
|
100
116
|
|
|
101
117
|
/**
|
|
102
|
-
* Fetches ad creative
|
|
118
|
+
* Fetches ad creative data (without tracking impression here)
|
|
103
119
|
*/
|
|
104
120
|
const fetchAd = useCallback(async () => {
|
|
105
121
|
if (!isInitialized) return;
|
|
@@ -107,6 +123,8 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
107
123
|
try {
|
|
108
124
|
setIsLoading(true);
|
|
109
125
|
setError(null);
|
|
126
|
+
setHasImpression(false);
|
|
127
|
+
setHasView(false);
|
|
110
128
|
|
|
111
129
|
const response = await Adgeist.fetchCreative(
|
|
112
130
|
apiKey,
|
|
@@ -119,24 +137,12 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
119
137
|
const creative: { data: AdData } = response as { data: AdData };
|
|
120
138
|
setAdData(creative.data);
|
|
121
139
|
onAdLoadSuccess?.(creative.data);
|
|
122
|
-
|
|
123
|
-
if (creative.data.seatBid.length > 0) {
|
|
124
|
-
await Adgeist.sendCreativeAnalytic(
|
|
125
|
-
creative.data.seatBid?.[0]?.bid?.[0]?.id || '',
|
|
126
|
-
dataAdSlot,
|
|
127
|
-
publisherId,
|
|
128
|
-
'IMPRESSION',
|
|
129
|
-
domain,
|
|
130
|
-
apiKey,
|
|
131
|
-
creative.data.id,
|
|
132
|
-
isTestEnvironment
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
140
|
} catch (err) {
|
|
136
141
|
const error = err instanceof Error ? err : new Error('Ad load failed');
|
|
137
142
|
setError(error);
|
|
143
|
+
setHasImpression(false);
|
|
144
|
+
setHasView(false);
|
|
138
145
|
onAdLoadError?.(error);
|
|
139
|
-
console.error('Ad load failed:', error);
|
|
140
146
|
} finally {
|
|
141
147
|
setIsLoading(false);
|
|
142
148
|
}
|
|
@@ -152,43 +158,188 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
152
158
|
]);
|
|
153
159
|
|
|
154
160
|
/**
|
|
155
|
-
*
|
|
161
|
+
* Tracks impression when media (image/video) is fully loaded
|
|
156
162
|
*/
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
159
|
-
const bidId = adData.seatBid[0]?.bid[0]?.id;
|
|
160
|
-
if (!bidId) return;
|
|
163
|
+
const trackImpressionOnMediaLoad = useCallback(async () => {
|
|
164
|
+
if (hasImpression || !bidId || !campaignId) return;
|
|
161
165
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
try {
|
|
167
|
+
const renderTime = Date.now() - renderStartTime.current;
|
|
168
|
+
|
|
169
|
+
await Adgeist.trackImpression(
|
|
170
|
+
campaignId,
|
|
171
|
+
adSpaceId,
|
|
172
|
+
publisherId,
|
|
173
|
+
apiKey,
|
|
174
|
+
bidId,
|
|
175
|
+
isTestEnvironment,
|
|
176
|
+
renderTime
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
setHasImpression(true);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error('Failed to track impression:', err);
|
|
182
|
+
}
|
|
183
|
+
}, [
|
|
184
|
+
hasImpression,
|
|
185
|
+
bidId,
|
|
186
|
+
campaignId,
|
|
187
|
+
adSpaceId,
|
|
188
|
+
publisherId,
|
|
189
|
+
apiKey,
|
|
190
|
+
isTestEnvironment,
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Tracks view event for banner ads when visible for >=1s and >=50% in viewport
|
|
195
|
+
*/
|
|
196
|
+
const trackView = useCallback(async () => {
|
|
197
|
+
if (hasView || !hasImpression || !bidId || !campaignId) return;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const currentViewTime = viewTime.current;
|
|
201
|
+
const currentTimeToVisible = timeToVisible.current || 0;
|
|
202
|
+
const visibility = currentVisibilityRatio.current;
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
currentViewTime >= (dataSlotType === 'video' ? 2000 : 1000) &&
|
|
206
|
+
visibility >= 0.5 &&
|
|
207
|
+
timeToVisible.current !== null
|
|
208
|
+
) {
|
|
209
|
+
await Adgeist.trackView(
|
|
210
|
+
campaignId,
|
|
211
|
+
adSpaceId,
|
|
166
212
|
publisherId,
|
|
167
|
-
'CLICK',
|
|
168
|
-
domain,
|
|
169
213
|
apiKey,
|
|
170
|
-
|
|
171
|
-
isTestEnvironment
|
|
214
|
+
bidId,
|
|
215
|
+
isTestEnvironment,
|
|
216
|
+
currentViewTime,
|
|
217
|
+
visibility,
|
|
218
|
+
0,
|
|
219
|
+
currentTimeToVisible
|
|
172
220
|
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
221
|
+
|
|
222
|
+
setHasView(true);
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error('Failed to track view:', err);
|
|
226
|
+
}
|
|
227
|
+
}, [
|
|
228
|
+
hasView,
|
|
229
|
+
hasImpression,
|
|
230
|
+
bidId,
|
|
231
|
+
campaignId,
|
|
232
|
+
adSpaceId,
|
|
233
|
+
publisherId,
|
|
234
|
+
apiKey,
|
|
235
|
+
isTestEnvironment,
|
|
236
|
+
dataSlotType,
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Calculates visibility ratio and updates view metrics
|
|
241
|
+
*/
|
|
242
|
+
const checkVisibility = useCallback(() => {
|
|
243
|
+
if (!adRef.current || !hasImpression) return;
|
|
244
|
+
|
|
245
|
+
adRef.current.measure((_x, _y, _width, height, _pageX, pageY) => {
|
|
246
|
+
const window = Dimensions.get('window');
|
|
247
|
+
const windowHeight = window.height;
|
|
248
|
+
|
|
249
|
+
const adTop = pageY;
|
|
250
|
+
const adBottom = pageY + height;
|
|
251
|
+
const windowTop = 0;
|
|
252
|
+
const windowBottom = windowHeight;
|
|
253
|
+
|
|
254
|
+
const visibleTop = Math.max(adTop, windowTop);
|
|
255
|
+
const visibleBottom = Math.min(adBottom, windowBottom);
|
|
256
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
257
|
+
const visibilityRatio = height > 0 ? visibleHeight / height : 0;
|
|
258
|
+
currentVisibilityRatio.current = visibilityRatio;
|
|
259
|
+
|
|
260
|
+
const currentTime = Date.now();
|
|
261
|
+
const deltaTime = currentTime - lastCheckTime.current;
|
|
262
|
+
lastCheckTime.current = currentTime;
|
|
263
|
+
|
|
264
|
+
if (visibilityRatio >= 0.5 && timeToVisible.current === null) {
|
|
265
|
+
timeToVisible.current = currentTime - renderStartTime.current;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (visibilityRatio >= 0.5) {
|
|
269
|
+
viewTime.current += deltaTime;
|
|
270
|
+
if (!visibilityStartTime.current) {
|
|
271
|
+
visibilityStartTime.current = currentTime;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
visibilityStartTime.current = null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (
|
|
278
|
+
viewTime.current >= (dataSlotType === 'video' ? 2000 : 1000) &&
|
|
279
|
+
visibilityRatio >= 0.5
|
|
280
|
+
) {
|
|
281
|
+
trackView();
|
|
176
282
|
}
|
|
283
|
+
|
|
284
|
+
if (dataSlotType === 'video' && videoRef.current) {
|
|
285
|
+
if (visibilityRatio >= 0.5) {
|
|
286
|
+
isInView.current = true;
|
|
287
|
+
videoRef.current.seek(lastPausedTime.current || 0);
|
|
288
|
+
} else {
|
|
289
|
+
isInView.current = false;
|
|
290
|
+
lastPausedTime.current = videoRef.current.currentTime || 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}, [hasImpression, trackView, dataSlotType]);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Handles ad click and sends click analytics
|
|
298
|
+
*/
|
|
299
|
+
const handleClick = useCallback(async () => {
|
|
300
|
+
if (!adData || !adData.seatBid.length || !bidId || !campaignId) return;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
Adgeist.trackClick(
|
|
304
|
+
campaignId,
|
|
305
|
+
adSpaceId,
|
|
306
|
+
publisherId,
|
|
307
|
+
apiKey,
|
|
308
|
+
bidId,
|
|
309
|
+
isTestEnvironment
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
await Linking.openURL(normalizeUrl(creativeData.ctaUrl));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error('Failed to handle ad click:', err);
|
|
177
315
|
}
|
|
178
316
|
}, [
|
|
179
317
|
adData,
|
|
180
|
-
|
|
318
|
+
bidId,
|
|
319
|
+
campaignId,
|
|
320
|
+
adSpaceId,
|
|
181
321
|
publisherId,
|
|
182
|
-
domain,
|
|
183
322
|
apiKey,
|
|
184
323
|
isTestEnvironment,
|
|
185
324
|
creativeData,
|
|
186
325
|
]);
|
|
187
326
|
|
|
188
327
|
useEffect(() => {
|
|
328
|
+
renderStartTime.current = Date.now();
|
|
329
|
+
lastCheckTime.current = Date.now();
|
|
189
330
|
fetchAd();
|
|
190
331
|
}, [fetchAd]);
|
|
191
332
|
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!hasImpression || hasView) return;
|
|
335
|
+
|
|
336
|
+
const intervalId = setInterval(checkVisibility, 200);
|
|
337
|
+
|
|
338
|
+
return () => {
|
|
339
|
+
clearInterval(intervalId);
|
|
340
|
+
};
|
|
341
|
+
}, [hasImpression, hasView, checkVisibility]);
|
|
342
|
+
|
|
192
343
|
if (isLoading) {
|
|
193
344
|
return (
|
|
194
345
|
<View
|
|
@@ -211,32 +362,42 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
211
362
|
style={{ width: '100%', height: '100%' }}
|
|
212
363
|
>
|
|
213
364
|
<View
|
|
365
|
+
ref={adRef}
|
|
214
366
|
style={[
|
|
215
367
|
styles.adContainer,
|
|
216
368
|
!isResponsive && { width: width, height: height },
|
|
217
369
|
]}
|
|
218
370
|
>
|
|
219
371
|
{dataSlotType === 'banner' ? (
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
accessibilityLabel="Ad Creative"
|
|
224
|
-
resizeMode="contain"
|
|
225
|
-
onError={(e) =>
|
|
226
|
-
console.error('Image load error:', e.nativeEvent.error)
|
|
227
|
-
}
|
|
228
|
-
/>
|
|
229
|
-
) : (
|
|
230
|
-
<View style={styles.videoCreative}>
|
|
231
|
-
<Video
|
|
372
|
+
<TouchableWithoutFeedback onPress={handleClick}>
|
|
373
|
+
<Image
|
|
374
|
+
style={styles.creative}
|
|
232
375
|
source={{ uri: creativeData.creativeUrl }}
|
|
376
|
+
accessibilityLabel="Ad Creative"
|
|
233
377
|
resizeMode="contain"
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
378
|
+
onLoad={trackImpressionOnMediaLoad}
|
|
379
|
+
onError={(e) => {
|
|
380
|
+
console.error('Image load error:', e.nativeEvent.error);
|
|
381
|
+
setError(new Error('Failed to load ad image'));
|
|
382
|
+
}}
|
|
238
383
|
/>
|
|
239
|
-
|
|
384
|
+
</TouchableWithoutFeedback>
|
|
385
|
+
) : (
|
|
386
|
+
<View style={styles.videoCreative}>
|
|
387
|
+
<TouchableWithoutFeedback onPress={handleClick}>
|
|
388
|
+
<Video
|
|
389
|
+
ref={videoRef}
|
|
390
|
+
source={{ uri: creativeData.creativeUrl }}
|
|
391
|
+
resizeMode="contain"
|
|
392
|
+
style={{ width: '100%', height: '100%' }}
|
|
393
|
+
repeat={true}
|
|
394
|
+
muted={isMuted}
|
|
395
|
+
onLoad={trackImpressionOnMediaLoad}
|
|
396
|
+
onError={() => {
|
|
397
|
+
setError(new Error('Failed to load ad video'));
|
|
398
|
+
}}
|
|
399
|
+
/>
|
|
400
|
+
</TouchableWithoutFeedback>
|
|
240
401
|
<TouchableWithoutFeedback onPress={() => setIsMuted(!isMuted)}>
|
|
241
402
|
<Image
|
|
242
403
|
style={styles.soundIcon}
|
|
@@ -256,7 +417,6 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
256
417
|
<Text style={styles.title} numberOfLines={1} ellipsizeMode="tail">
|
|
257
418
|
{creativeData.creativeTitle}
|
|
258
419
|
</Text>
|
|
259
|
-
|
|
260
420
|
<Text
|
|
261
421
|
style={styles.description}
|
|
262
422
|
numberOfLines={1}
|
|
@@ -264,20 +424,19 @@ export const BannerAd: React.FC<AdBannerProps> = ({
|
|
|
264
424
|
>
|
|
265
425
|
{creativeData.creativeDescription}
|
|
266
426
|
</Text>
|
|
267
|
-
|
|
268
427
|
<Text
|
|
269
428
|
style={styles.brandName}
|
|
270
429
|
numberOfLines={1}
|
|
271
430
|
ellipsizeMode="tail"
|
|
272
431
|
>
|
|
273
|
-
{creativeData?.creativeBrandName || 'Brand Name'}
|
|
432
|
+
{creativeData?.creativeBrandName || 'The Brand Name'}
|
|
274
433
|
</Text>
|
|
275
434
|
</View>
|
|
276
435
|
<TouchableWithoutFeedback onPress={handleClick}>
|
|
277
436
|
<Image
|
|
278
437
|
style={styles.linkButton}
|
|
279
438
|
source={{
|
|
280
|
-
uri: 'https://d2cfeg6k9cklz9.cloudfront.net/
|
|
439
|
+
uri: 'https://d2cfeg6k9cklz9.cloudfront.net/ad-icons/Button.png',
|
|
281
440
|
}}
|
|
282
441
|
accessibilityLabel="Visit Advertiser Site"
|
|
283
442
|
/>
|
|
@@ -324,31 +483,35 @@ const styles = StyleSheet.create({
|
|
|
324
483
|
flexDirection: 'row',
|
|
325
484
|
justifyContent: 'space-between',
|
|
326
485
|
paddingVertical: 10,
|
|
327
|
-
|
|
486
|
+
paddingRight: 10,
|
|
487
|
+
paddingLeft: 20,
|
|
328
488
|
alignItems: 'center',
|
|
329
489
|
borderBottomLeftRadius: 5,
|
|
330
490
|
borderBottomRightRadius: 5,
|
|
331
491
|
},
|
|
332
492
|
contentContainer: {
|
|
333
|
-
|
|
493
|
+
flex: 1,
|
|
494
|
+
marginRight: 10,
|
|
334
495
|
},
|
|
335
496
|
title: {
|
|
336
497
|
color: '#1A1A1A',
|
|
337
|
-
fontSize:
|
|
498
|
+
fontSize: 16,
|
|
338
499
|
fontWeight: '600',
|
|
339
500
|
},
|
|
340
501
|
description: {
|
|
341
502
|
color: '#4A4A4A',
|
|
342
|
-
fontSize:
|
|
503
|
+
fontSize: 15,
|
|
343
504
|
marginBottom: 4,
|
|
344
505
|
},
|
|
345
506
|
brandName: {
|
|
346
507
|
color: '#6B7280',
|
|
347
508
|
fontSize: 14,
|
|
348
|
-
textTransform: '
|
|
509
|
+
textTransform: 'capitalize',
|
|
510
|
+
opacity: 0.8,
|
|
349
511
|
},
|
|
350
512
|
linkButton: {
|
|
351
|
-
width:
|
|
513
|
+
width: 80,
|
|
352
514
|
height: 40,
|
|
515
|
+
objectFit: 'contain',
|
|
353
516
|
},
|
|
354
517
|
});
|
package/src/utilities.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a URL to ensure it has a valid protocol
|
|
3
|
+
*/
|
|
4
|
+
export const normalizeUrl = (url: string) => {
|
|
5
|
+
if (!url) return url;
|
|
6
|
+
if (url.startsWith('www.')) {
|
|
7
|
+
return `https://${url}`;
|
|
8
|
+
}
|
|
9
|
+
if (!url.match(/^[a-zA-Z]+:\/\//)) {
|
|
10
|
+
return `https://${url}`;
|
|
11
|
+
}
|
|
12
|
+
return url;
|
|
13
|
+
};
|