@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 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.20.0",
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
- export interface AdBannerProps {
8
- platformId: string;
9
- apiKey: string;
10
- size?: 'leaderboard' | 'medium-rectangle' | 'wide-skyscraper' | 'banner';
11
- apiUrl?: string; // Optional explicit API URL override
12
- userId?: string | null;
13
- userSegment?: string | null;
14
- className?: string;
15
- clickMetadata?: Record<string, unknown>; // Optional metadata to send with click events
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
18
- }
19
-
20
- export function AdBanner({
21
- platformId,
22
- apiKey,
23
- size = 'banner',
24
- apiUrl,
25
- userId = null,
26
- userSegment = null,
27
- className = '',
28
- clickMetadata,
29
- impressionMetadata,
30
- showImpressionFinished = false,
31
- }: AdBannerProps) {
32
- const [ad, setAd] = useState<Ad | null>(null);
33
- const [loading, setLoading] = useState(true);
34
- const [error, setError] = useState<string | null>(null);
35
- const [devMode, setDevMode] = useState(false);
36
- const [impressionStatus, setImpressionStatus] = useState<'pending' | 'counting' | 'completed'>('pending');
37
- const impressionTimerRef = useRef<NodeJS.Timeout | null>(null);
38
- const hasTrackedImpressionRef = useRef<string | null>(null); // Track which ad ID we've already tracked
39
-
40
- useEffect(() => {
41
- const loadAd = async () => {
42
- try {
43
- setLoading(true);
44
- setError(null);
45
-
46
- const response = await fetchAdBanner({
47
- platformId,
48
- apiKey,
49
- size,
50
- apiUrl,
51
- userId,
52
- userSegment,
53
- });
54
-
55
- // Only log in development mode
56
- if (process.env.NODE_ENV === 'development') {
57
- console.log('[AdBanner] Processed response:', {
58
- success: response.success,
59
- hasAd: !!response.ad,
60
- devMode: response.dev_mode,
61
- message: response.message,
62
- _dev_note: response._dev_note,
63
- fullResponse: response,
64
- });
65
- }
66
-
67
- if (response.success && response.ad) {
68
- if (process.env.NODE_ENV === 'development') {
69
- console.log('[AdBanner] Setting ad:', response.ad);
70
- }
71
- setAd(response.ad);
72
- setDevMode(response.dev_mode || false);
73
-
74
- // Only track impression once per ad - check if we've already tracked this ad ID
75
- if (hasTrackedImpressionRef.current !== response.ad.id) {
76
- hasTrackedImpressionRef.current = response.ad.id;
77
-
78
- // Start impression timer if showImpressionFinished is enabled
79
- if (showImpressionFinished) {
80
- setImpressionStatus('counting');
81
- // Standard impression timer: 2 seconds of view time
82
- impressionTimerRef.current = setTimeout(() => {
83
- setImpressionStatus('completed');
84
- impressionTimerRef.current = null;
85
- }, 2000);
86
-
87
- // Track impression (will be skipped in dev mode)
88
- // Note: We don't wait for the API call to complete - the timer determines when to show green check
89
- trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata).catch(() => {
90
- // Silently handle tracking errors - timer will still complete
91
- });
92
- } else {
93
- // Track impression without timer UI
94
- trackImpression(response.ad.id, apiKey, apiUrl, impressionMetadata);
95
- }
96
- }
97
- } else {
98
- // Create a more detailed error message
99
- const errorDetails = [];
100
- if (!response.success) errorDetails.push('API returned success=false');
101
- if (!response.ad) errorDetails.push('No ad object in response');
102
- if (response.message) errorDetails.push(`Message: ${response.message}`);
103
- if (response._dev_note) errorDetails.push(`Note: ${response._dev_note}`);
104
-
105
- const errorMsg = errorDetails.length > 0
106
- ? `No ads available: ${errorDetails.join(', ')}`
107
- : 'No ads available';
108
-
109
- console.error('[AdBanner] No ad available - Full details:', {
110
- success: response.success,
111
- hasAd: !!response.ad,
112
- message: response.message,
113
- _dev_note: response._dev_note,
114
- fullResponse: response,
115
- errorDetails,
116
- });
117
- setError(errorMsg);
118
- }
119
- } catch (err) {
120
- console.error('[AdBanner] Error fetching ad:', err);
121
-
122
- // For CORS errors, show user-friendly message but log technical details
123
- if (err instanceof Error && err.message.includes('CORS error')) {
124
- console.error('[AdBanner] CORS Error Details:', {
125
- message: err.message,
126
- 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.',
127
- });
128
- // Show generic message to end users
129
- setError('Ad unavailable');
130
- } else {
131
- // For other errors, show the error message
132
- setError(err instanceof Error ? err.message : 'Failed to fetch ad');
133
- }
134
- } finally {
135
- setLoading(false);
136
- }
137
- };
138
-
139
- loadAd();
140
-
141
- // Cleanup timer on unmount or when dependencies change
142
- return () => {
143
- if (impressionTimerRef.current) {
144
- clearTimeout(impressionTimerRef.current);
145
- impressionTimerRef.current = null;
146
- }
147
- // Reset impression tracking ref when component unmounts or key dependencies change
148
- // This allows re-tracking if the ad ID changes (new ad loaded)
149
- hasTrackedImpressionRef.current = null;
150
- };
151
- }, [platformId, apiKey, size, apiUrl, userId, userSegment, showImpressionFinished]);
152
-
153
- const handleClick = () => {
154
- if (ad) {
155
- trackClick(ad.id, apiKey, apiUrl, clickMetadata);
156
- // The link will handle navigation
157
- }
158
- };
159
-
160
- if (loading) {
161
- return (
162
- <div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
163
- <span className="text-sm text-gray-500">Loading ad...</span>
164
- </div>
165
- );
166
- }
167
-
168
- if (error || !ad) {
169
- return (
170
- <div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
171
- <span className="text-sm text-gray-500">{error || 'Ad unavailable'}</span>
172
- </div>
173
- );
174
- }
175
-
176
- return (
177
- <div className={`relative ${className}`}>
178
- <a
179
- href={ad.targetUrl}
180
- onClick={handleClick}
181
- target="_blank"
182
- rel="noopener noreferrer"
183
- className="block"
184
- >
185
- {(ad.type === 'image' || ad.type === 'standard') && ad.contentUrl ? (
186
- <Image
187
- src={ad.contentUrl}
188
- alt={ad.name}
189
- width={ad.width || 300}
190
- height={ad.height || 250}
191
- className="rounded"
192
- unoptimized
193
- />
194
- ) : (
195
- <div className="flex items-center justify-center bg-gray-200 dark:bg-gray-700 rounded" style={{ width: ad.width || 300, height: ad.height || 250 }}>
196
- <span className="text-sm text-gray-500">Ad content</span>
197
- </div>
198
- )}
199
- </a>
200
- {showImpressionFinished && impressionStatus !== 'pending' && (
201
- <div className="absolute top-2 right-2">
202
- {impressionStatus === 'counting' ? (
203
- <svg
204
- className="w-5 h-5 text-gray-400 animate-spin"
205
- xmlns="http://www.w3.org/2000/svg"
206
- fill="none"
207
- viewBox="0 0 24 24"
208
- >
209
- <circle
210
- className="opacity-25"
211
- cx="12"
212
- cy="12"
213
- r="10"
214
- stroke="currentColor"
215
- strokeWidth="4"
216
- />
217
- <path
218
- className="opacity-75"
219
- fill="currentColor"
220
- 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"
221
- />
222
- </svg>
223
- ) : impressionStatus === 'completed' ? (
224
- <svg
225
- className="w-5 h-5 text-green-500"
226
- xmlns="http://www.w3.org/2000/svg"
227
- viewBox="0 0 20 20"
228
- fill="currentColor"
229
- >
230
- <path
231
- fillRule="evenodd"
232
- 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"
233
- clipRule="evenodd"
234
- />
235
- </svg>
236
- ) : null}
237
- </div>
238
- )}
239
- {devMode && (
240
- <p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
241
- )}
242
- </div>
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;