@thead-vantage/react 2.9.0 → 2.11.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/AdBanner.tsx +13 -1
- package/src/lib/ads.ts +105 -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
|
@@ -83,7 +83,19 @@ export function AdBanner({
|
|
|
83
83
|
}
|
|
84
84
|
} catch (err) {
|
|
85
85
|
console.error('[AdBanner] Error fetching ad:', err);
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
// For CORS errors, show user-friendly message but log technical details
|
|
88
|
+
if (err instanceof Error && err.message.includes('CORS error')) {
|
|
89
|
+
console.error('[AdBanner] CORS Error Details:', {
|
|
90
|
+
message: err.message,
|
|
91
|
+
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.',
|
|
92
|
+
});
|
|
93
|
+
// Show generic message to end users
|
|
94
|
+
setError('Ad unavailable');
|
|
95
|
+
} else {
|
|
96
|
+
// For other errors, show the error message
|
|
97
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch ad');
|
|
98
|
+
}
|
|
87
99
|
} finally {
|
|
88
100
|
setLoading(false);
|
|
89
101
|
}
|
package/src/lib/ads.ts
CHANGED
|
@@ -127,36 +127,76 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
127
127
|
searchParams.set('no_click_track', 'true'); // Prevent click tracking
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Build the full URL -
|
|
131
|
-
//
|
|
130
|
+
// Build the full URL - always make direct requests to thead-vantage.com
|
|
131
|
+
// Normalize URL to avoid redirects that cause CORS preflight issues
|
|
132
132
|
let fullApiUrl = apiBaseUrl;
|
|
133
133
|
if (!fullApiUrl.includes('/api/ads')) {
|
|
134
134
|
fullApiUrl = fullApiUrl.replace(/\/$/, '') + '/api/ads';
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
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()}`;
|
|
138
147
|
|
|
139
148
|
// Log the request (without exposing the API key)
|
|
140
149
|
const logUrl = url.replace(new RegExp(`api_key=${params.apiKey}`, 'g'), 'api_key=***');
|
|
141
150
|
console.log('[AdBanner] Fetching ad from:', logUrl);
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
});
|
|
150
169
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
183
|
|
|
155
|
-
|
|
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
|
+
}
|
|
156
194
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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:', {
|
|
160
200
|
success: data.success,
|
|
161
201
|
hasAd: !!data.ad,
|
|
162
202
|
hasAds: !!(data.ads && Array.isArray(data.ads)),
|
|
@@ -167,14 +207,14 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
167
207
|
adsType: data.ads ? (Array.isArray(data.ads) ? 'array' : typeof data.ads) : 'none',
|
|
168
208
|
});
|
|
169
209
|
|
|
170
|
-
|
|
171
|
-
|
|
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 => {
|
|
172
212
|
if (typeof obj !== 'object' || obj === null) return false;
|
|
173
213
|
return 'name' in obj && 'type' in obj && 'contentUrl' in obj && 'targetUrl' in obj;
|
|
174
214
|
};
|
|
175
215
|
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
// Helper to convert AdData to Ad
|
|
217
|
+
const convertToAd = (adData: AdData): Ad => {
|
|
178
218
|
return {
|
|
179
219
|
id: adData.id,
|
|
180
220
|
name: adData.alt || 'Ad',
|
|
@@ -186,16 +226,16 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
186
226
|
};
|
|
187
227
|
};
|
|
188
228
|
|
|
189
|
-
|
|
190
|
-
|
|
229
|
+
// Helper to normalize ad type - "standard" should be treated as "image"
|
|
230
|
+
const normalizeAdType = (ad: Ad): Ad => {
|
|
191
231
|
if (ad.type === 'standard' && ad.contentUrl) {
|
|
192
232
|
return { ...ad, type: 'image' };
|
|
193
233
|
}
|
|
194
234
|
return ad;
|
|
195
235
|
};
|
|
196
236
|
|
|
197
|
-
|
|
198
|
-
|
|
237
|
+
// Handle both single ad and array of ads
|
|
238
|
+
if (data.ads && Array.isArray(data.ads) && data.ads.length > 0) {
|
|
199
239
|
// If we get an array, use the first ad and ensure it's Ad type
|
|
200
240
|
const firstAd = data.ads[0];
|
|
201
241
|
console.log('[AdBanner] Processing ads array, first ad:', JSON.stringify(firstAd, null, 2));
|
|
@@ -219,8 +259,8 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
219
259
|
};
|
|
220
260
|
}
|
|
221
261
|
|
|
222
|
-
|
|
223
|
-
|
|
262
|
+
// Handle single ad response - convert AdData to Ad
|
|
263
|
+
if (data.ad) {
|
|
224
264
|
console.log('[AdBanner] Processing single ad:', JSON.stringify(data.ad, null, 2));
|
|
225
265
|
|
|
226
266
|
// Check if it's already in Ad format or needs conversion
|
|
@@ -244,8 +284,8 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
244
284
|
};
|
|
245
285
|
}
|
|
246
286
|
|
|
247
|
-
|
|
248
|
-
|
|
287
|
+
// No ad found - log detailed info
|
|
288
|
+
console.warn('[AdBanner] No ad found in response:', {
|
|
249
289
|
success: data.success,
|
|
250
290
|
hasAd: !!data.ad,
|
|
251
291
|
hasAds: !!(data.ads && Array.isArray(data.ads)),
|
|
@@ -254,33 +294,45 @@ export async function fetchAdBanner(params: FetchAdBannerParams): Promise<AdBann
|
|
|
254
294
|
fullData: data,
|
|
255
295
|
});
|
|
256
296
|
|
|
257
|
-
return {
|
|
258
|
-
success: data.success || false,
|
|
259
|
-
dev_mode: data.dev_mode,
|
|
260
|
-
_dev_note: data._dev_note || 'No ad data in response',
|
|
261
|
-
message: data.message || 'No ads available in response',
|
|
262
|
-
};
|
|
263
|
-
} catch (error) {
|
|
264
|
-
console.error('[AdBanner] Error fetching ad:', error);
|
|
265
|
-
|
|
266
|
-
// In TheAd Vantage dev mode, return mock data
|
|
267
|
-
if (process.env.NEXT_PUBLIC_THEAD_VANTAGE_DEV_MODE === 'true') {
|
|
268
297
|
return {
|
|
269
|
-
success:
|
|
270
|
-
dev_mode:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
name: 'Development Ad',
|
|
274
|
-
type: 'image',
|
|
275
|
-
contentUrl: '/placeholder-ad.png',
|
|
276
|
-
targetUrl: '#',
|
|
277
|
-
width: 300,
|
|
278
|
-
height: 250,
|
|
279
|
-
},
|
|
280
|
-
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',
|
|
281
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;
|
|
282
332
|
}
|
|
283
|
-
|
|
333
|
+
} catch (error) {
|
|
334
|
+
// Outer catch for any errors not caught by inner try-catch
|
|
335
|
+
console.error('[AdBanner] Unexpected error:', error);
|
|
284
336
|
throw error;
|
|
285
337
|
}
|
|
286
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;
|