@thead-vantage/react 2.20.0 → 2.22.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 +26 -0
- package/package.json +2 -2
- package/src/components/AdBanner.tsx +294 -245
- package/src/lib/ads.ts +26 -0
- package/src/lib/thead-vantage-config.ts +13 -0
package/README.md
CHANGED
|
@@ -154,6 +154,7 @@ The main component for displaying ads from TheAd Vantage.
|
|
|
154
154
|
| `clickMetadata` | `Record<string, unknown>` | No | - | Optional metadata to send with click tracking events |
|
|
155
155
|
| `impressionMetadata` | `Record<string, unknown>` | No | - | Optional metadata to send with impression tracking events |
|
|
156
156
|
| `showImpressionFinished` | `boolean` | No | `false` | Show spinning checkmark during impression timer (2s), then green checkmark when complete |
|
|
157
|
+
| `development` | `boolean` | No | `false` | Explicitly enable development mode (disables tracking, handles CORS errors gracefully) |
|
|
157
158
|
|
|
158
159
|
**Example**:
|
|
159
160
|
```tsx
|
|
@@ -262,6 +263,31 @@ The `showImpressionFinished` prop enables a visual indicator that shows when an
|
|
|
262
263
|
|
|
263
264
|
This feature is useful for debugging and providing visual feedback that impressions are being tracked correctly.
|
|
264
265
|
|
|
266
|
+
### Development Mode
|
|
267
|
+
|
|
268
|
+
The `development` prop explicitly enables development mode, which:
|
|
269
|
+
|
|
270
|
+
- **Disables tracking**: Impressions and clicks are not recorded (even if the API call succeeds)
|
|
271
|
+
- **Handles CORS gracefully**: CORS errors are logged as warnings instead of breaking the component
|
|
272
|
+
- **Prevents error display**: Network errors don't show error messages to users in development mode
|
|
273
|
+
|
|
274
|
+
**Use Case**: When developing locally and you can't configure CORS for localhost, use this prop to prevent CORS errors from breaking your development workflow.
|
|
275
|
+
|
|
276
|
+
**Example**:
|
|
277
|
+
```tsx
|
|
278
|
+
<AdBanner
|
|
279
|
+
platformId="10"
|
|
280
|
+
apiKey="abc123xyz"
|
|
281
|
+
size="banner"
|
|
282
|
+
development={process.env.NODE_ENV === 'development'}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Note**: When `development={true}`, the component will:
|
|
287
|
+
- Automatically skip all tracking (impressions and clicks)
|
|
288
|
+
- Log CORS errors as warnings instead of throwing errors
|
|
289
|
+
- Continue to work even if the API is unreachable (won't show error state)
|
|
290
|
+
|
|
265
291
|
### Utility Functions
|
|
266
292
|
|
|
267
293
|
You can also use the utility functions directly:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thead-vantage/react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@tailwindcss/postcss": "^4",
|
|
63
|
-
"@types/node": "^20",
|
|
63
|
+
"@types/node": "^20.19.27",
|
|
64
64
|
"@types/react": "^19",
|
|
65
65
|
"@types/react-dom": "^19",
|
|
66
66
|
"next": "^16.1.1",
|
|
@@ -1,245 +1,294 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState, useRef } from 'react';
|
|
4
|
-
import Image from 'next/image';
|
|
5
|
-
import { fetchAdBanner, trackImpression, trackClick, type Ad } from '../lib/ads';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const [
|
|
36
|
-
const [
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { fetchAdBanner, trackImpression, trackClick, type Ad } from '../lib/ads';
|
|
6
|
+
import { setTheadVantageConfig } from '../lib/thead-vantage-config';
|
|
7
|
+
|
|
8
|
+
export interface AdBannerProps {
|
|
9
|
+
platformId: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
size?: 'leaderboard' | 'medium-rectangle' | 'wide-skyscraper' | 'banner';
|
|
12
|
+
apiUrl?: string; // Optional explicit API URL override
|
|
13
|
+
userId?: string | null;
|
|
14
|
+
userSegment?: string | null;
|
|
15
|
+
className?: string;
|
|
16
|
+
clickMetadata?: Record<string, unknown>; // Optional metadata to send with click events
|
|
17
|
+
impressionMetadata?: Record<string, unknown>; // Optional metadata to send with impression events
|
|
18
|
+
showImpressionFinished?: boolean; // Show spinning checkmark during impression timer, then green check when complete
|
|
19
|
+
development?: boolean; // Explicitly enable development mode (disables tracking, handles CORS gracefully)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AdBanner({
|
|
23
|
+
platformId,
|
|
24
|
+
apiKey,
|
|
25
|
+
size = 'banner',
|
|
26
|
+
apiUrl,
|
|
27
|
+
userId = null,
|
|
28
|
+
userSegment = null,
|
|
29
|
+
className = '',
|
|
30
|
+
clickMetadata,
|
|
31
|
+
impressionMetadata,
|
|
32
|
+
showImpressionFinished = false,
|
|
33
|
+
development = false,
|
|
34
|
+
}: AdBannerProps) {
|
|
35
|
+
const [ad, setAd] = useState<Ad | null>(null);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
const [devMode, setDevMode] = useState(false);
|
|
39
|
+
const [impressionStatus, setImpressionStatus] = useState<'pending' | 'counting' | 'completed'>('pending');
|
|
40
|
+
const impressionTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
41
|
+
const hasTrackedImpressionRef = useRef<string | null>(null); // Track which ad ID we've already tracked
|
|
42
|
+
|
|
43
|
+
// Set development mode in config if explicitly enabled
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (development) {
|
|
46
|
+
setTheadVantageConfig({ devMode: true });
|
|
47
|
+
}
|
|
48
|
+
}, [development]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const loadAd = async () => {
|
|
52
|
+
try {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
|
|
56
|
+
const response = await fetchAdBanner({
|
|
57
|
+
platformId,
|
|
58
|
+
apiKey,
|
|
59
|
+
size,
|
|
60
|
+
apiUrl,
|
|
61
|
+
userId,
|
|
62
|
+
userSegment,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Only log in development mode
|
|
66
|
+
if (process.env.NODE_ENV === 'development') {
|
|
67
|
+
console.log('[AdBanner] Processed response:', {
|
|
68
|
+
success: response.success,
|
|
69
|
+
hasAd: !!response.ad,
|
|
70
|
+
devMode: response.dev_mode,
|
|
71
|
+
message: response.message,
|
|
72
|
+
_dev_note: response._dev_note,
|
|
73
|
+
fullResponse: response,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (response.success && response.ad) {
|
|
78
|
+
if (process.env.NODE_ENV === 'development') {
|
|
79
|
+
console.log('[AdBanner] Setting ad:', response.ad);
|
|
80
|
+
}
|
|
81
|
+
setAd(response.ad);
|
|
82
|
+
setDevMode(response.dev_mode || false);
|
|
83
|
+
|
|
84
|
+
// Only track impression once per ad - check if we've already tracked this ad ID
|
|
85
|
+
if (hasTrackedImpressionRef.current !== response.ad.id) {
|
|
86
|
+
hasTrackedImpressionRef.current = response.ad.id;
|
|
87
|
+
|
|
88
|
+
// Start impression timer if showImpressionFinished is enabled
|
|
89
|
+
if (showImpressionFinished) {
|
|
90
|
+
setImpressionStatus('counting');
|
|
91
|
+
// Standard impression timer: 2 seconds of view time
|
|
92
|
+
impressionTimerRef.current = setTimeout(() => {
|
|
93
|
+
setImpressionStatus('completed');
|
|
94
|
+
impressionTimerRef.current = null;
|
|
95
|
+
}, 2000);
|
|
96
|
+
|
|
97
|
+
// Track impression (will be skipped in dev mode)
|
|
98
|
+
// Note: We don't wait for the API call to complete - the timer determines when to show green check
|
|
99
|
+
trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata).catch(() => {
|
|
100
|
+
// Silently handle tracking errors - timer will still complete
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
// Track impression without timer UI
|
|
104
|
+
trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Create a more detailed error message
|
|
109
|
+
const errorDetails = [];
|
|
110
|
+
if (!response.success) errorDetails.push('API returned success=false');
|
|
111
|
+
if (!response.ad) errorDetails.push('No ad object in response');
|
|
112
|
+
if (response.message) errorDetails.push(`Message: ${response.message}`);
|
|
113
|
+
if (response._dev_note) errorDetails.push(`Note: ${response._dev_note}`);
|
|
114
|
+
|
|
115
|
+
const errorMsg = errorDetails.length > 0
|
|
116
|
+
? `No ads available: ${errorDetails.join(', ')}`
|
|
117
|
+
: 'No ads available';
|
|
118
|
+
|
|
119
|
+
console.error('[AdBanner] No ad available - Full details:', {
|
|
120
|
+
success: response.success,
|
|
121
|
+
hasAd: !!response.ad,
|
|
122
|
+
message: response.message,
|
|
123
|
+
_dev_note: response._dev_note,
|
|
124
|
+
fullResponse: response,
|
|
125
|
+
errorDetails,
|
|
126
|
+
});
|
|
127
|
+
setError(errorMsg);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('[AdBanner] Error fetching ad:', err);
|
|
131
|
+
|
|
132
|
+
// Check if we're on localhost
|
|
133
|
+
const isLocalhost = typeof window !== 'undefined' &&
|
|
134
|
+
(window.location.hostname === 'localhost' ||
|
|
135
|
+
window.location.hostname === '127.0.0.1' ||
|
|
136
|
+
window.location.hostname === '[::1]');
|
|
137
|
+
|
|
138
|
+
// For CORS errors, handle differently in development mode or localhost
|
|
139
|
+
if (err instanceof Error && err.message.includes('CORS error')) {
|
|
140
|
+
if (development || isLocalhost) {
|
|
141
|
+
// In development mode or localhost, show TheAd Vantage logo as fallback ad
|
|
142
|
+
console.warn('[AdBanner] CORS error in development mode - showing TheAd Vantage fallback ad. Tracking is disabled.');
|
|
143
|
+
// Create a fallback ad with TheAd Vantage logo
|
|
144
|
+
setAd({
|
|
145
|
+
id: 'thead-vantage-fallback',
|
|
146
|
+
name: 'TheAd Vantage',
|
|
147
|
+
type: 'image',
|
|
148
|
+
contentUrl: '/TheAd-Vantage-Logo.png',
|
|
149
|
+
targetUrl: 'https://www.thead-vantage.com',
|
|
150
|
+
width: 300,
|
|
151
|
+
height: 250,
|
|
152
|
+
});
|
|
153
|
+
setDevMode(true);
|
|
154
|
+
setError(null); // Don't show error in dev mode
|
|
155
|
+
} else {
|
|
156
|
+
console.error('[AdBanner] CORS Error Details:', {
|
|
157
|
+
message: err.message,
|
|
158
|
+
note: 'This is a server-side configuration issue. The thead-vantage.com server needs to handle OPTIONS requests without redirecting. See CORS_CONFIGURATION.md for implementation details.',
|
|
159
|
+
});
|
|
160
|
+
// Show generic message to end users
|
|
161
|
+
setError('Ad unavailable');
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// For other errors, show the error message (unless in development or localhost)
|
|
165
|
+
if (development || isLocalhost) {
|
|
166
|
+
console.warn('[AdBanner] Error in development mode - showing TheAd Vantage fallback ad:', err);
|
|
167
|
+
// Create a fallback ad with TheAd Vantage logo
|
|
168
|
+
setAd({
|
|
169
|
+
id: 'thead-vantage-fallback',
|
|
170
|
+
name: 'TheAd Vantage',
|
|
171
|
+
type: 'image',
|
|
172
|
+
contentUrl: '/TheAd-Vantage-Logo.png',
|
|
173
|
+
targetUrl: 'https://www.thead-vantage.com',
|
|
174
|
+
width: 300,
|
|
175
|
+
height: 250,
|
|
176
|
+
});
|
|
177
|
+
setDevMode(true);
|
|
178
|
+
setError(null); // Don't show errors in dev mode
|
|
179
|
+
} else {
|
|
180
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch ad');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
setLoading(false);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
loadAd();
|
|
189
|
+
|
|
190
|
+
// Cleanup timer on unmount or when dependencies change
|
|
191
|
+
return () => {
|
|
192
|
+
if (impressionTimerRef.current) {
|
|
193
|
+
clearTimeout(impressionTimerRef.current);
|
|
194
|
+
impressionTimerRef.current = null;
|
|
195
|
+
}
|
|
196
|
+
// Reset impression tracking ref when component unmounts or key dependencies change
|
|
197
|
+
// This allows re-tracking if the ad ID changes (new ad loaded)
|
|
198
|
+
hasTrackedImpressionRef.current = null;
|
|
199
|
+
};
|
|
200
|
+
}, [platformId, apiKey, size, apiUrl, userId, userSegment, showImpressionFinished, development]);
|
|
201
|
+
|
|
202
|
+
const handleClick = () => {
|
|
203
|
+
if (ad) {
|
|
204
|
+
trackClick(ad.id, apiKey, apiUrl, clickMetadata);
|
|
205
|
+
// The link will handle navigation
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (loading) {
|
|
210
|
+
return (
|
|
211
|
+
<div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
|
|
212
|
+
<span className="text-sm text-gray-500">Loading ad...</span>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (error || !ad) {
|
|
218
|
+
return (
|
|
219
|
+
<div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
|
|
220
|
+
<span className="text-sm text-gray-500">{error || 'Ad unavailable'}</span>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className={`relative ${className}`}>
|
|
227
|
+
<a
|
|
228
|
+
href={ad.targetUrl}
|
|
229
|
+
onClick={handleClick}
|
|
230
|
+
target="_blank"
|
|
231
|
+
rel="noopener noreferrer"
|
|
232
|
+
className="block"
|
|
233
|
+
>
|
|
234
|
+
{(ad.type === 'image' || ad.type === 'standard') && ad.contentUrl ? (
|
|
235
|
+
<Image
|
|
236
|
+
src={ad.contentUrl}
|
|
237
|
+
alt={ad.name}
|
|
238
|
+
width={ad.width || 300}
|
|
239
|
+
height={ad.height || 250}
|
|
240
|
+
className="rounded"
|
|
241
|
+
unoptimized
|
|
242
|
+
/>
|
|
243
|
+
) : (
|
|
244
|
+
<div className="flex items-center justify-center bg-gray-200 dark:bg-gray-700 rounded" style={{ width: ad.width || 300, height: ad.height || 250 }}>
|
|
245
|
+
<span className="text-sm text-gray-500">Ad content</span>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</a>
|
|
249
|
+
{showImpressionFinished && impressionStatus !== 'pending' && (
|
|
250
|
+
<div className="absolute top-2 right-2">
|
|
251
|
+
{impressionStatus === 'counting' ? (
|
|
252
|
+
<svg
|
|
253
|
+
className="w-5 h-5 text-gray-400 animate-spin"
|
|
254
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
255
|
+
fill="none"
|
|
256
|
+
viewBox="0 0 24 24"
|
|
257
|
+
>
|
|
258
|
+
<circle
|
|
259
|
+
className="opacity-25"
|
|
260
|
+
cx="12"
|
|
261
|
+
cy="12"
|
|
262
|
+
r="10"
|
|
263
|
+
stroke="currentColor"
|
|
264
|
+
strokeWidth="4"
|
|
265
|
+
/>
|
|
266
|
+
<path
|
|
267
|
+
className="opacity-75"
|
|
268
|
+
fill="currentColor"
|
|
269
|
+
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"
|
|
270
|
+
/>
|
|
271
|
+
</svg>
|
|
272
|
+
) : impressionStatus === 'completed' ? (
|
|
273
|
+
<svg
|
|
274
|
+
className="w-5 h-5 text-green-500"
|
|
275
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
276
|
+
viewBox="0 0 20 20"
|
|
277
|
+
fill="currentColor"
|
|
278
|
+
>
|
|
279
|
+
<path
|
|
280
|
+
fillRule="evenodd"
|
|
281
|
+
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"
|
|
282
|
+
clipRule="evenodd"
|
|
283
|
+
/>
|
|
284
|
+
</svg>
|
|
285
|
+
) : null}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
{devMode && (
|
|
289
|
+
<p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
package/src/lib/ads.ts
CHANGED
|
@@ -321,8 +321,34 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
321
321
|
message: data.message || 'No ads available in response',
|
|
322
322
|
};
|
|
323
323
|
} catch (error) {
|
|
324
|
+
// Check if we're on localhost (client-side only)
|
|
325
|
+
const isLocalhost = typeof window !== 'undefined' &&
|
|
326
|
+
(window.location.hostname === 'localhost' ||
|
|
327
|
+
window.location.hostname === '127.0.0.1' ||
|
|
328
|
+
window.location.hostname === '[::1]');
|
|
329
|
+
|
|
324
330
|
// Handle CORS errors specifically
|
|
325
331
|
if (error instanceof TypeError && (error.message.includes('CORS') || error.message.includes('Failed to fetch'))) {
|
|
332
|
+
// On localhost, return TheAd Vantage fallback ad instead of throwing
|
|
333
|
+
if (isLocalhost) {
|
|
334
|
+
console.warn('[AdBanner] CORS error on localhost - returning TheAd Vantage fallback ad. Tracking is disabled.');
|
|
335
|
+
return {
|
|
336
|
+
success: true,
|
|
337
|
+
dev_mode: true,
|
|
338
|
+
ad: {
|
|
339
|
+
id: 'thead-vantage-fallback',
|
|
340
|
+
name: 'TheAd Vantage',
|
|
341
|
+
type: 'image',
|
|
342
|
+
contentUrl: '/TheAd-Vantage-Logo.png',
|
|
343
|
+
targetUrl: 'https://www.thead-vantage.com',
|
|
344
|
+
width: 300,
|
|
345
|
+
height: 250,
|
|
346
|
+
},
|
|
347
|
+
message: 'Localhost development: Using TheAd Vantage fallback ad due to CORS',
|
|
348
|
+
_dev_note: 'CORS error on localhost - showing fallback ad. This is expected in development.',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
326
352
|
// Check if this might be a 308 redirect issue
|
|
327
353
|
// Vercel redirects thead-vantage.com → www.thead-vantage.com at edge level
|
|
328
354
|
// This breaks CORS preflight because browsers don't allow redirects on OPTIONS requests
|
|
@@ -148,8 +148,21 @@ export function isDevelopmentMode(): boolean {
|
|
|
148
148
|
* - Running on localhost (platform developers testing locally)
|
|
149
149
|
* - OR in TheAd Vantage dev mode
|
|
150
150
|
* - OR in general development mode
|
|
151
|
+
* - OR explicitly set via runtime config (development prop)
|
|
151
152
|
*/
|
|
152
153
|
export function shouldUseDevFlags(): boolean {
|
|
154
|
+
// Check runtime config first (set via development prop)
|
|
155
|
+
if (typeof window !== 'undefined') {
|
|
156
|
+
const runtimeDevMode = (window as any).__THEAD_VANTAGE_DEV_MODE__;
|
|
157
|
+
if (runtimeDevMode === true) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Also check global config
|
|
162
|
+
if (globalConfig.devMode === true) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
// TheAd Vantage dev mode always uses dev flags
|
|
154
167
|
if (isTheadVantageDevMode()) {
|
|
155
168
|
return true;
|