@thead-vantage/react 2.8.0 → 2.10.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 +1 -1
- package/src/components/AdDisplay.tsx +15 -6
- package/src/lib/ads.ts +108 -53
- package/src/lib/thead-vantage-config.ts +24 -2
package/README.md
CHANGED
|
@@ -510,3 +510,29 @@ import { fetchAdBanner, trackImpression, trackClick } from '@thead-vantage/react
|
|
|
510
510
|
| Platform Developer | `localhost:3000` | `thead-vantage.com` | ❌ No (dev flags) | None (auto) |
|
|
511
511
|
| Platform Developer | Custom platform | Custom URL | ✅ Yes | `NEXT_PUBLIC_THEAD_VANTAGE_API_URL` |
|
|
512
512
|
| TheAd Vantage Dev | `localhost:3000` | `localhost:3001` | ❌ No | `NEXT_PUBLIC_THEAD_VANTAGE_DEV_MODE=true` |
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## CORS Configuration for Production
|
|
517
|
+
|
|
518
|
+
**Important**: The `thead-vantage.com` server needs to be configured to allow CORS requests from registered production platforms.
|
|
519
|
+
|
|
520
|
+
### For Platform Developers
|
|
521
|
+
|
|
522
|
+
When deploying your platform to production:
|
|
523
|
+
|
|
524
|
+
1. **Contact TheAd Vantage support** to register your production domain(s)
|
|
525
|
+
2. **Provide your production URLs** (e.g., `https://minotsbugle.com`, `https://www.minotsbugle.com`)
|
|
526
|
+
3. **Verify CORS is working** by checking browser console for CORS errors
|
|
527
|
+
|
|
528
|
+
The TheAd Vantage team will add your domains to the allowed origins list for your platform's API key.
|
|
529
|
+
|
|
530
|
+
### For TheAd Vantage Platform Developers
|
|
531
|
+
|
|
532
|
+
See `CORS_CONFIGURATION.md` for:
|
|
533
|
+
- Database schema for storing allowed origins per platform
|
|
534
|
+
- CORS middleware implementation examples
|
|
535
|
+
- Security best practices
|
|
536
|
+
- Testing procedures
|
|
537
|
+
|
|
538
|
+
The CORS system allows each platform to have multiple allowed origins stored in the database, and the middleware validates requests against these origins before setting CORS headers.
|
package/package.json
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import Image from 'next/image';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fetchAds,
|
|
7
|
+
trackImpression,
|
|
8
|
+
trackClick,
|
|
9
|
+
type AdData,
|
|
10
|
+
type AdsResponse
|
|
11
|
+
} from '../lib/ads';
|
|
6
12
|
|
|
7
13
|
interface AdDisplayProps {
|
|
8
14
|
position?: string;
|
|
@@ -21,12 +27,15 @@ export default function AdDisplay({ position, className = '' }: AdDisplayProps)
|
|
|
21
27
|
setLoading(true);
|
|
22
28
|
setError(null);
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
// Build params object - fetchAds accepts Record<string, string> | undefined
|
|
31
|
+
// Note: fetchAds is different from fetchAdBanner (which requires platformId/apiKey)
|
|
32
|
+
const params: Record<string, string> | undefined = position
|
|
33
|
+
? { position }
|
|
34
|
+
: undefined;
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
// Explicitly type to avoid confusion with fetchAdBanner's FetchAdBannerParams
|
|
37
|
+
const fetchAdsTyped: (params?: Record<string, string>) => Promise<AdsResponse> = fetchAds;
|
|
38
|
+
const response = await fetchAdsTyped(params);
|
|
30
39
|
|
|
31
40
|
if (response.success && response.ad) {
|
|
32
41
|
setAd(response.ad);
|
package/src/lib/ads.ts
CHANGED
|
@@ -56,6 +56,9 @@ export interface FetchAdBannerParams {
|
|
|
56
56
|
/**
|
|
57
57
|
* Fetch ads from TheAd Vantage platform (generic)
|
|
58
58
|
* In development mode, uses mock data and prevents tracking
|
|
59
|
+
*
|
|
60
|
+
* @param params - Optional query parameters as key-value pairs
|
|
61
|
+
* @returns Promise resolving to AdsResponse
|
|
59
62
|
*/
|
|
60
63
|
export async function fetchAds(params?: Record<string, string>): Promise<AdsResponse> {
|
|
61
64
|
try {
|
|
@@ -124,36 +127,76 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
124
127
|
searchParams.set('no_click_track', 'true'); // Prevent click tracking
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
// Build the full URL -
|
|
128
|
-
//
|
|
130
|
+
// Build the full URL - always make direct requests to thead-vantage.com
|
|
131
|
+
// Normalize URL to avoid redirects that cause CORS preflight issues
|
|
129
132
|
let fullApiUrl = apiBaseUrl;
|
|
130
133
|
if (!fullApiUrl.includes('/api/ads')) {
|
|
131
134
|
fullApiUrl = fullApiUrl.replace(/\/$/, '') + '/api/ads';
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
// Normalize URL to HTTPS and ensure consistent format
|
|
138
|
+
// This prevents HTTP→HTTPS redirects that break CORS preflight
|
|
139
|
+
let normalizedUrl = fullApiUrl;
|
|
140
|
+
if (normalizedUrl.startsWith('http://')) {
|
|
141
|
+
normalizedUrl = normalizedUrl.replace('http://', 'https://');
|
|
142
|
+
}
|
|
143
|
+
// Ensure no trailing slash issues
|
|
144
|
+
normalizedUrl = normalizedUrl.replace(/\/api\/ads\/$/, '/api/ads');
|
|
145
|
+
|
|
146
|
+
const url = `${normalizedUrl}?${searchParams.toString()}`;
|
|
135
147
|
|
|
136
148
|
// Log the request (without exposing the API key)
|
|
137
149
|
const logUrl = url.replace(new RegExp(`api_key=${params.apiKey}`, 'g'), 'api_key=***');
|
|
138
150
|
console.log('[AdBanner] Fetching ad from:', logUrl);
|
|
139
151
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
// Make direct request to thead-vantage.com
|
|
153
|
+
// Try to make a "simple request" that doesn't trigger preflight
|
|
154
|
+
// Simple requests don't trigger preflight if they:
|
|
155
|
+
// - Use GET, HEAD, or POST method
|
|
156
|
+
// - Only have simple headers (Accept, Accept-Language, Content-Language, Content-Type with specific values)
|
|
157
|
+
// - Don't have custom headers
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetch(url, {
|
|
160
|
+
method: 'GET',
|
|
161
|
+
mode: 'cors', // Explicitly enable CORS
|
|
162
|
+
credentials: 'omit', // Don't send cookies
|
|
163
|
+
headers: {
|
|
164
|
+
'Accept': 'application/json', // Simple header - shouldn't trigger preflight
|
|
165
|
+
},
|
|
166
|
+
redirect: 'manual', // Handle redirects manually to avoid preflight redirect issues
|
|
167
|
+
cache: 'no-cache',
|
|
168
|
+
});
|
|
147
169
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
// Handle redirects manually (if any)
|
|
171
|
+
if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) {
|
|
172
|
+
const location = response.headers.get('location');
|
|
173
|
+
if (location) {
|
|
174
|
+
// Follow the redirect for the actual request (not preflight)
|
|
175
|
+
const redirectUrl = location.startsWith('http')
|
|
176
|
+
? location
|
|
177
|
+
: new URL(location, normalizedUrl).toString();
|
|
178
|
+
console.warn('[AdBanner] Server redirected request, following redirect:', redirectUrl.replace(new RegExp(`api_key=${params.apiKey}`, 'g'), 'api_key=***'));
|
|
179
|
+
// Recursively call with the redirect URL
|
|
180
|
+
return await fetchAdBanner({ ...params, apiUrl: redirectUrl });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
151
183
|
|
|
152
|
-
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
186
|
+
console.error('[AdBanner] API Error:', {
|
|
187
|
+
status: response.status,
|
|
188
|
+
statusText: response.statusText,
|
|
189
|
+
url: logUrl,
|
|
190
|
+
errorText: errorText.substring(0, 200), // Limit error text length
|
|
191
|
+
});
|
|
192
|
+
throw new Error(`Failed to fetch ad: ${response.status} ${response.statusText}`);
|
|
193
|
+
}
|
|
153
194
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
const data: AdsResponse = await response.json();
|
|
196
|
+
|
|
197
|
+
// Debug logging - log the FULL response to see exactly what we're getting
|
|
198
|
+
console.log('[AdBanner] Full API Response:', JSON.stringify(data, null, 2));
|
|
199
|
+
console.log('[AdBanner] API Response Summary:', {
|
|
157
200
|
success: data.success,
|
|
158
201
|
hasAd: !!data.ad,
|
|
159
202
|
hasAds: !!(data.ads && Array.isArray(data.ads)),
|
|
@@ -164,14 +207,14 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
164
207
|
adsType: data.ads ? (Array.isArray(data.ads) ? 'array' : typeof data.ads) : 'none',
|
|
165
208
|
});
|
|
166
209
|
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
// Type guard to check if an object is Ad type (for arrays that might contain either)
|
|
211
|
+
const isAd = (obj: unknown): obj is Ad => {
|
|
169
212
|
if (typeof obj !== 'object' || obj === null) return false;
|
|
170
213
|
return 'name' in obj && 'type' in obj && 'contentUrl' in obj && 'targetUrl' in obj;
|
|
171
214
|
};
|
|
172
215
|
|
|
173
|
-
|
|
174
|
-
|
|
216
|
+
// Helper to convert AdData to Ad
|
|
217
|
+
const convertToAd = (adData: AdData): Ad => {
|
|
175
218
|
return {
|
|
176
219
|
id: adData.id,
|
|
177
220
|
name: adData.alt || 'Ad',
|
|
@@ -183,16 +226,16 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
183
226
|
};
|
|
184
227
|
};
|
|
185
228
|
|
|
186
|
-
|
|
187
|
-
|
|
229
|
+
// Helper to normalize ad type - "standard" should be treated as "image"
|
|
230
|
+
const normalizeAdType = (ad: Ad): Ad => {
|
|
188
231
|
if (ad.type === 'standard' && ad.contentUrl) {
|
|
189
232
|
return { ...ad, type: 'image' };
|
|
190
233
|
}
|
|
191
234
|
return ad;
|
|
192
235
|
};
|
|
193
236
|
|
|
194
|
-
|
|
195
|
-
|
|
237
|
+
// Handle both single ad and array of ads
|
|
238
|
+
if (data.ads && Array.isArray(data.ads) && data.ads.length > 0) {
|
|
196
239
|
// If we get an array, use the first ad and ensure it's Ad type
|
|
197
240
|
const firstAd = data.ads[0];
|
|
198
241
|
console.log('[AdBanner] Processing ads array, first ad:', JSON.stringify(firstAd, null, 2));
|
|
@@ -216,8 +259,8 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
216
259
|
};
|
|
217
260
|
}
|
|
218
261
|
|
|
219
|
-
|
|
220
|
-
|
|
262
|
+
// Handle single ad response - convert AdData to Ad
|
|
263
|
+
if (data.ad) {
|
|
221
264
|
console.log('[AdBanner] Processing single ad:', JSON.stringify(data.ad, null, 2));
|
|
222
265
|
|
|
223
266
|
// Check if it's already in Ad format or needs conversion
|
|
@@ -241,8 +284,8 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
241
284
|
};
|
|
242
285
|
}
|
|
243
286
|
|
|
244
|
-
|
|
245
|
-
|
|
287
|
+
// No ad found - log detailed info
|
|
288
|
+
console.warn('[AdBanner] No ad found in response:', {
|
|
246
289
|
success: data.success,
|
|
247
290
|
hasAd: !!data.ad,
|
|
248
291
|
hasAds: !!(data.ads && Array.isArray(data.ads)),
|
|
@@ -251,33 +294,45 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
251
294
|
fullData: data,
|
|
252
295
|
});
|
|
253
296
|
|
|
254
|
-
return {
|
|
255
|
-
success: data.success || false,
|
|
256
|
-
dev_mode: data.dev_mode,
|
|
257
|
-
_dev_note: data._dev_note || 'No ad data in response',
|
|
258
|
-
message: data.message || 'No ads available in response',
|
|
259
|
-
};
|
|
260
|
-
} catch (error) {
|
|
261
|
-
console.error('[AdBanner] Error fetching ad:', error);
|
|
262
|
-
|
|
263
|
-
// In TheAd Vantage dev mode, return mock data
|
|
264
|
-
if (process.env.NEXT_PUBLIC_THEAD_VANTAGE_DEV_MODE === 'true') {
|
|
265
297
|
return {
|
|
266
|
-
success:
|
|
267
|
-
dev_mode:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
name: 'Development Ad',
|
|
271
|
-
type: 'image',
|
|
272
|
-
contentUrl: '/placeholder-ad.png',
|
|
273
|
-
targetUrl: '#',
|
|
274
|
-
width: 300,
|
|
275
|
-
height: 250,
|
|
276
|
-
},
|
|
277
|
-
message: 'TheAd Vantage dev mode: Using mock ad',
|
|
298
|
+
success: data.success || false,
|
|
299
|
+
dev_mode: data.dev_mode,
|
|
300
|
+
_dev_note: data._dev_note || 'No ad data in response',
|
|
301
|
+
message: data.message || 'No ads available in response',
|
|
278
302
|
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// Handle CORS errors specifically
|
|
305
|
+
if (error instanceof TypeError && (error.message.includes('CORS') || error.message.includes('Failed to fetch'))) {
|
|
306
|
+
console.error('[AdBanner] CORS error detected. This usually means the server is redirecting preflight requests.');
|
|
307
|
+
console.error('[AdBanner] The server at thead-vantage.com needs to handle OPTIONS requests directly without redirecting.');
|
|
308
|
+
throw new Error('CORS error: The ad server is redirecting preflight requests. This needs to be fixed server-side. See CORS_CONFIGURATION.md for details.');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.error('[AdBanner] Error fetching ad:', error);
|
|
312
|
+
|
|
313
|
+
// In TheAd Vantage dev mode, return mock data
|
|
314
|
+
if (process.env.NEXT_PUBLIC_THEAD_VANTAGE_DEV_MODE === 'true') {
|
|
315
|
+
return {
|
|
316
|
+
success: true,
|
|
317
|
+
dev_mode: true,
|
|
318
|
+
ad: {
|
|
319
|
+
id: 'dev-ad-1',
|
|
320
|
+
name: 'Development Ad',
|
|
321
|
+
type: 'image',
|
|
322
|
+
contentUrl: '/placeholder-ad.png',
|
|
323
|
+
targetUrl: '#',
|
|
324
|
+
width: 300,
|
|
325
|
+
height: 250,
|
|
326
|
+
},
|
|
327
|
+
message: 'TheAd Vantage dev mode: Using mock ad',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
throw error;
|
|
279
332
|
}
|
|
280
|
-
|
|
333
|
+
} catch (error) {
|
|
334
|
+
// Outer catch for any errors not caught by inner try-catch
|
|
335
|
+
console.error('[AdBanner] Unexpected error:', error);
|
|
281
336
|
throw error;
|
|
282
337
|
}
|
|
283
338
|
}
|
|
@@ -97,9 +97,31 @@ export function getApiBaseUrl(explicitApiUrl?: string): string {
|
|
|
97
97
|
reason = 'production mode (default)';
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Normalize the URL to avoid redirects that cause CORS preflight issues
|
|
101
|
+
if (selectedUrl) {
|
|
102
|
+
// Normalize to HTTPS (avoid HTTP→HTTPS redirects)
|
|
103
|
+
if (selectedUrl.startsWith('http://')) {
|
|
104
|
+
selectedUrl = selectedUrl.replace('http://', 'https://');
|
|
105
|
+
}
|
|
106
|
+
// Ensure consistent trailing slash handling
|
|
107
|
+
// Remove trailing slash, then ensure /api/ads is present
|
|
108
|
+
selectedUrl = selectedUrl.replace(/\/$/, '');
|
|
109
|
+
if (!selectedUrl.endsWith('/api/ads')) {
|
|
110
|
+
// If it doesn't end with /api/ads, add it
|
|
111
|
+
selectedUrl = selectedUrl + '/api/ads';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
// Log which URL was selected (helpful for debugging)
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
// Always log in browser context, but only in development for server-side
|
|
117
|
+
if (typeof window !== 'undefined') {
|
|
118
|
+
if (process.env.NODE_ENV === 'development') {
|
|
119
|
+
console.log(`[TheAd Vantage] API Base URL selected: ${selectedUrl} (reason: ${reason})`);
|
|
120
|
+
} else {
|
|
121
|
+
// In production, log to console but only if there's an issue (helps with debugging)
|
|
122
|
+
// We'll log errors separately, but this helps identify configuration issues
|
|
123
|
+
console.debug(`[TheAd Vantage] API Base URL: ${selectedUrl}`);
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
return selectedUrl;
|