@thead-vantage/react 2.18.0 → 2.19.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/README.md CHANGED
@@ -151,12 +151,24 @@ The main component for displaying ads from TheAd Vantage.
151
151
  | `userId` | `string \| null` | No | `null` | User ID for ad targeting |
152
152
  | `userSegment` | `string \| null` | No | `null` | User segment for ad targeting |
153
153
  | `className` | `string` | No | `''` | Additional CSS classes |
154
+ | `clickMetadata` | `Record<string, unknown>` | No | - | Optional metadata to send with click tracking events |
155
+ | `impressionMetadata` | `Record<string, unknown>` | No | - | Optional metadata to send with impression tracking events |
156
+ | `showImpressionFinished` | `boolean` | No | `false` | Show spinning checkmark during impression timer (2s), then green checkmark when complete |
154
157
 
155
158
  **Example**:
156
159
  ```tsx
157
160
  import { AdBanner } from '@/components/AdBanner';
158
161
 
159
162
  export default function ArticlePage() {
163
+ const userData = {
164
+ id: 'user-123',
165
+ email: 'user@example.com',
166
+ school: 'University of Example',
167
+ grade: 'Senior',
168
+ major: 'Computer Science',
169
+ role: 'student',
170
+ };
171
+
160
172
  return (
161
173
  <div>
162
174
  <article>
@@ -169,9 +181,26 @@ export default function ArticlePage() {
169
181
  platformId="10"
170
182
  apiKey="abc123xyz"
171
183
  size="medium-rectangle"
172
- userId="user-123"
184
+ userId={userData.id}
173
185
  userSegment="premium"
174
186
  className="my-4"
187
+ clickMetadata={{
188
+ userId: userData.id,
189
+ email: userData.email,
190
+ school: userData.school,
191
+ grade: userData.grade,
192
+ major: userData.major,
193
+ role: userData.role,
194
+ }}
195
+ impressionMetadata={{
196
+ userId: userData.id,
197
+ email: userData.email,
198
+ school: userData.school,
199
+ grade: userData.grade,
200
+ major: userData.major,
201
+ role: userData.role,
202
+ }}
203
+ showImpressionFinished={true}
175
204
  />
176
205
  </aside>
177
206
  </div>
@@ -179,6 +208,60 @@ export default function ArticlePage() {
179
208
  }
180
209
  ```
181
210
 
211
+ ### Metadata Tracking
212
+
213
+ The `AdBanner` component supports optional metadata that is sent with click and impression tracking events. This allows you to pass additional context about the user or session to TheAd Vantage for analytics purposes.
214
+
215
+ **Metadata Props**:
216
+ - `clickMetadata`: Optional object with key-value pairs sent with click events
217
+ - `impressionMetadata`: Optional object with key-value pairs sent with impression events
218
+
219
+ **Example with Metadata**:
220
+ ```tsx
221
+ <AdBanner
222
+ platformId="10"
223
+ apiKey="abc123xyz"
224
+ size="medium-rectangle"
225
+ clickMetadata={{
226
+ userId: userData?.id,
227
+ email: userData?.email,
228
+ school: userData?.school,
229
+ grade: userData?.grade,
230
+ major: userData?.major,
231
+ role: userData?.role,
232
+ }}
233
+ impressionMetadata={{
234
+ userId: userData?.id,
235
+ email: userData?.email,
236
+ school: userData?.school,
237
+ grade: userData?.grade,
238
+ major: userData?.major,
239
+ role: userData?.role,
240
+ }}
241
+ />
242
+ ```
243
+
244
+ **Note**: The metadata is sent to the `/api/ads/track` endpoint on thead-vantage.com. Ensure the API has been updated to support receiving metadata (see `other/thead-vantage-api-metadata-prompt.md` for implementation details).
245
+
246
+ ### Impression Finished Indicator
247
+
248
+ The `showImpressionFinished` prop enables a visual indicator that shows when an impression has been recorded:
249
+
250
+ - **Spinning Checkmark**: A gray spinning checkmark appears in the top-right corner of the ad during the 2-second impression timer
251
+ - **Green Checkmark**: After 2 seconds, the checkmark turns green to indicate the impression has been recorded
252
+
253
+ **Example**:
254
+ ```tsx
255
+ <AdBanner
256
+ platformId="10"
257
+ apiKey="abc123xyz"
258
+ size="banner"
259
+ showImpressionFinished={true}
260
+ />
261
+ ```
262
+
263
+ This feature is useful for debugging and providing visual feedback that impressions are being tracked correctly.
264
+
182
265
  ### Utility Functions
183
266
 
184
267
  You can also use the utility functions directly:
@@ -198,8 +281,16 @@ const response = await fetchAdBanner({
198
281
  // Track events (automatically skipped in TheAd Vantage dev mode)
199
282
  // Note: trackImpression and trackClick now require apiKey parameter
200
283
  // They automatically include client fingerprinting data for fraud detection
201
- trackImpression(adId, apiKey);
202
- trackClick(adId, apiKey);
284
+ // Optional metadata can be passed as the last parameter
285
+ trackImpression(adId, apiKey, apiUrl, {
286
+ userId: 'user-123',
287
+ email: 'user@example.com',
288
+ school: 'University of Example',
289
+ });
290
+ trackClick(adId, apiKey, apiUrl, {
291
+ userId: 'user-123',
292
+ email: 'user@example.com',
293
+ });
203
294
 
204
295
  // You can also collect fingerprint data manually if needed
205
296
  const fingerprint = collectFingerprint();
@@ -335,14 +426,16 @@ The TheAd Vantage integration uses a smart mode detection system to support diff
335
426
 
336
427
  ### Components
337
428
  - **`src/components/AdBanner.tsx`**: Main React component for displaying ads
338
- - Props: `platformId`, `apiKey`, `size`, `apiUrl?`, `userId?`, `userSegment?`, `className?`
429
+ - Props: `platformId`, `apiKey`, `size`, `apiUrl?`, `userId?`, `userSegment?`, `className?`, `clickMetadata?`, `impressionMetadata?`, `showImpressionFinished?`
339
430
  - Automatically handles loading, error states, and dev mode indicators
431
+ - Supports optional metadata for click and impression tracking
432
+ - Optional impression finished indicator (spinning checkmark → green checkmark)
340
433
 
341
434
  ### Utilities
342
435
  - **`src/lib/ads.ts`**: Utility functions for fetching and tracking ads
343
436
  - `fetchAdBanner(params)`: Fetches ads with full parameter support
344
- - `trackImpression(adId, apiKey, apiUrl?)`: Tracks ad impressions with fingerprinting (skipped in dev mode)
345
- - `trackClick(adId, apiKey, apiUrl?)`: Tracks ad clicks with fingerprinting (skipped in dev mode)
437
+ - `trackImpression(adId, apiKey, apiUrl?, metadata?)`: Tracks ad impressions with fingerprinting and optional metadata (skipped in dev mode)
438
+ - `trackClick(adId, apiKey, apiUrl?, metadata?)`: Tracks ad clicks with fingerprinting and optional metadata (skipped in dev mode)
346
439
  - `collectFingerprint()`: Collects client fingerprinting data for fraud detection
347
440
 
348
441
  ## Implementation Instructions for AI Agents
@@ -415,7 +508,38 @@ The system uses this priority order to determine which API URL to use:
415
508
  />
416
509
  ```
417
510
 
418
- **Pattern 3: Custom API URL Override**
511
+ **Pattern 3: Ad with Metadata Tracking**
512
+ ```tsx
513
+ <AdBanner
514
+ platformId="10"
515
+ apiKey="key"
516
+ size="medium-rectangle"
517
+ clickMetadata={{
518
+ userId: userData?.id,
519
+ email: userData?.email,
520
+ school: userData?.school,
521
+ grade: userData?.grade,
522
+ }}
523
+ impressionMetadata={{
524
+ userId: userData?.id,
525
+ email: userData?.email,
526
+ school: userData?.school,
527
+ grade: userData?.grade,
528
+ }}
529
+ />
530
+ ```
531
+
532
+ **Pattern 4: Ad with Impression Finished Indicator**
533
+ ```tsx
534
+ <AdBanner
535
+ platformId="10"
536
+ apiKey="key"
537
+ size="banner"
538
+ showImpressionFinished={true}
539
+ />
540
+ ```
541
+
542
+ **Pattern 5: Custom API URL Override**
419
543
  ```tsx
420
544
  <AdBanner
421
545
  platformId="10"
@@ -425,7 +549,7 @@ The system uses this priority order to determine which API URL to use:
425
549
  />
426
550
  ```
427
551
 
428
- **Pattern 4: Programmatic Ad Fetching**
552
+ **Pattern 6: Programmatic Ad Fetching**
429
553
  ```tsx
430
554
  const response = await fetchAdBanner({
431
555
  platformId: '10',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thead-vantage/react",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "React components and utilities for TheAd Vantage ad platform integration",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useState, useRef } from 'react';
4
4
  import Image from 'next/image';
5
5
  import { fetchAdBanner, trackImpression, trackClick, type Ad } from '../lib/ads';
6
6
 
@@ -14,6 +14,7 @@ export interface AdBannerProps {
14
14
  className?: string;
15
15
  clickMetadata?: Record<string, unknown>; // Optional metadata to send with click events
16
16
  impressionMetadata?: Record<string, unknown>; // Optional metadata to send with impression events
17
+ showImpressionFinished?: boolean; // Show spinning checkmark during impression timer, then green check when complete
17
18
  }
18
19
 
19
20
  export function AdBanner({
@@ -26,11 +27,14 @@ export function AdBanner({
26
27
  className = '',
27
28
  clickMetadata,
28
29
  impressionMetadata,
30
+ showImpressionFinished = false,
29
31
  }: AdBannerProps) {
30
32
  const [ad, setAd] = useState<Ad | null>(null);
31
33
  const [loading, setLoading] = useState(true);
32
34
  const [error, setError] = useState<string | null>(null);
33
35
  const [devMode, setDevMode] = useState(false);
36
+ const [impressionStatus, setImpressionStatus] = useState<'pending' | 'counting' | 'completed'>('pending');
37
+ const impressionTimerRef = useRef<NodeJS.Timeout | null>(null);
34
38
 
35
39
  useEffect(() => {
36
40
  const loadAd = async () => {
@@ -66,8 +70,24 @@ export function AdBanner({
66
70
  setAd(response.ad);
67
71
  setDevMode(response.dev_mode || false);
68
72
 
69
- // Track impression (will be skipped in dev mode)
70
- trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata);
73
+ // Start impression timer if showImpressionFinished is enabled
74
+ if (showImpressionFinished) {
75
+ setImpressionStatus('counting');
76
+ // Standard impression timer: 2 seconds of view time
77
+ impressionTimerRef.current = setTimeout(() => {
78
+ setImpressionStatus('completed');
79
+ impressionTimerRef.current = null;
80
+ }, 2000);
81
+
82
+ // Track impression (will be skipped in dev mode)
83
+ // Note: We don't wait for the API call to complete - the timer determines when to show green check
84
+ trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata).catch(() => {
85
+ // Silently handle tracking errors - timer will still complete
86
+ });
87
+ } else {
88
+ // Track impression without timer UI
89
+ trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata);
90
+ }
71
91
  } else {
72
92
  // Create a more detailed error message
73
93
  const errorDetails = [];
@@ -111,7 +131,15 @@ export function AdBanner({
111
131
  };
112
132
 
113
133
  loadAd();
114
- }, [platformId, apiKey, size, apiUrl, userId, userSegment]);
134
+
135
+ // Cleanup timer on unmount or when dependencies change
136
+ return () => {
137
+ if (impressionTimerRef.current) {
138
+ clearTimeout(impressionTimerRef.current);
139
+ impressionTimerRef.current = null;
140
+ }
141
+ };
142
+ }, [platformId, apiKey, size, apiUrl, userId, userSegment, showImpressionFinished, impressionMetadata]);
115
143
 
116
144
  const handleClick = () => {
117
145
  if (ad) {
@@ -137,7 +165,7 @@ export function AdBanner({
137
165
  }
138
166
 
139
167
  return (
140
- <div className={className}>
168
+ <div className={`relative ${className}`}>
141
169
  <a
142
170
  href={ad.targetUrl}
143
171
  onClick={handleClick}
@@ -160,6 +188,45 @@ export function AdBanner({
160
188
  </div>
161
189
  )}
162
190
  </a>
191
+ {showImpressionFinished && impressionStatus !== 'pending' && (
192
+ <div className="absolute top-2 right-2">
193
+ {impressionStatus === 'counting' ? (
194
+ <svg
195
+ className="w-5 h-5 text-gray-400 animate-spin"
196
+ xmlns="http://www.w3.org/2000/svg"
197
+ fill="none"
198
+ viewBox="0 0 24 24"
199
+ >
200
+ <circle
201
+ className="opacity-25"
202
+ cx="12"
203
+ cy="12"
204
+ r="10"
205
+ stroke="currentColor"
206
+ strokeWidth="4"
207
+ />
208
+ <path
209
+ className="opacity-75"
210
+ fill="currentColor"
211
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
212
+ />
213
+ </svg>
214
+ ) : impressionStatus === 'completed' ? (
215
+ <svg
216
+ className="w-5 h-5 text-green-500"
217
+ xmlns="http://www.w3.org/2000/svg"
218
+ viewBox="0 0 20 20"
219
+ fill="currentColor"
220
+ >
221
+ <path
222
+ fillRule="evenodd"
223
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
224
+ clipRule="evenodd"
225
+ />
226
+ </svg>
227
+ ) : null}
228
+ </div>
229
+ )}
163
230
  {devMode && (
164
231
  <p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
165
232
  )}