@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.
Files changed (28) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/generated/java/com/adgeist/NativeAdgeistSpec.java +83 -0
  3. package/android/generated/jni/CMakeLists.txt +36 -0
  4. package/android/generated/jni/RNAdgeistSpec-generated.cpp +98 -0
  5. package/android/generated/jni/RNAdgeistSpec.h +31 -0
  6. package/android/generated/jni/react/renderer/components/RNAdgeistSpec/RNAdgeistSpecJSI-generated.cpp +149 -0
  7. package/android/generated/jni/react/renderer/components/RNAdgeistSpec/RNAdgeistSpecJSI.h +170 -0
  8. package/android/src/main/java/com/adgeist/implementation/AdgeistModuleImpl.kt +101 -9
  9. package/android/src/newarch/java/com/AdgeistModule.kt +93 -2
  10. package/android/src/oldarch/java/com/AdgeistModule.kt +101 -4
  11. package/ios/generated/RNAdgeistSpec/RNAdgeistSpec-generated.mm +42 -7
  12. package/ios/generated/RNAdgeistSpec/RNAdgeistSpec.h +57 -10
  13. package/ios/generated/RNAdgeistSpecJSI-generated.cpp +81 -14
  14. package/ios/generated/RNAdgeistSpecJSI.h +54 -9
  15. package/lib/module/NativeAdgeist.js.map +1 -1
  16. package/lib/module/components/BannerAd.js +164 -46
  17. package/lib/module/components/BannerAd.js.map +1 -1
  18. package/lib/module/utilities.js +16 -0
  19. package/lib/module/utilities.js.map +1 -0
  20. package/lib/typescript/src/NativeAdgeist.d.ts +6 -1
  21. package/lib/typescript/src/NativeAdgeist.d.ts.map +1 -1
  22. package/lib/typescript/src/components/BannerAd.d.ts.map +1 -1
  23. package/lib/typescript/src/utilities.d.ts +5 -0
  24. package/lib/typescript/src/utilities.d.ts.map +1 -0
  25. package/package.json +1 -1
  26. package/src/NativeAdgeist.ts +61 -9
  27. package/src/components/BannerAd.tsx +226 -63
  28. 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>(false);
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 and sends impression analytics
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
- * Handles ad click and sends click analytics
161
+ * Tracks impression when media (image/video) is fully loaded
156
162
  */
157
- const handleClick = useCallback(async () => {
158
- if (adData && adData.seatBid.length > 0) {
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
- try {
163
- await Adgeist.sendCreativeAnalytic(
164
- bidId,
165
- dataAdSlot,
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
- adData.id,
171
- isTestEnvironment
214
+ bidId,
215
+ isTestEnvironment,
216
+ currentViewTime,
217
+ visibility,
218
+ 0,
219
+ currentTimeToVisible
172
220
  );
173
- await Linking.openURL(creativeData.ctaUrl);
174
- } catch (err) {
175
- console.error('Failed to handle ad click:', err);
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
- dataAdSlot,
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
- <Image
221
- style={styles.creative}
222
- source={{ uri: creativeData.creativeUrl }}
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
- style={{ width: '100%', height: '100%' }}
235
- repeat={true}
236
- muted={isMuted}
237
- onError={(e) => console.error('Video load error:', e)}
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/onboarding-icons/Button.png',
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
- paddingHorizontal: 20,
486
+ paddingRight: 10,
487
+ paddingLeft: 20,
328
488
  alignItems: 'center',
329
489
  borderBottomLeftRadius: 5,
330
490
  borderBottomRightRadius: 5,
331
491
  },
332
492
  contentContainer: {
333
- width: '80%',
493
+ flex: 1,
494
+ marginRight: 10,
334
495
  },
335
496
  title: {
336
497
  color: '#1A1A1A',
337
- fontSize: 18,
498
+ fontSize: 16,
338
499
  fontWeight: '600',
339
500
  },
340
501
  description: {
341
502
  color: '#4A4A4A',
342
- fontSize: 16,
503
+ fontSize: 15,
343
504
  marginBottom: 4,
344
505
  },
345
506
  brandName: {
346
507
  color: '#6B7280',
347
508
  fontSize: 14,
348
- textTransform: 'uppercase',
509
+ textTransform: 'capitalize',
510
+ opacity: 0.8,
349
511
  },
350
512
  linkButton: {
351
- width: 40,
513
+ width: 80,
352
514
  height: 40,
515
+ objectFit: 'contain',
353
516
  },
354
517
  });
@@ -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
+ };