@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thead-vantage/react",
3
- "version": "2.9.0",
3
+ "version": "2.11.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",
@@ -83,7 +83,19 @@ export function AdBanner({
83
83
  }
84
84
  } catch (err) {
85
85
  console.error('[AdBanner] Error fetching ad:', err);
86
- setError(err instanceof Error ? err.message : 'Failed to fetch ad');
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 - if apiBaseUrl already includes /api/ads, use it directly
131
- // Otherwise append /api/ads
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
- const url = `${fullApiUrl}?${searchParams.toString()}`;
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
- const response = await fetch(url, {
144
- method: 'GET',
145
- headers: {
146
- 'Content-Type': 'application/json',
147
- 'Accept': 'application/json',
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
- if (!response.ok) {
152
- throw new Error(`Failed to fetch ad: ${response.status} ${response.statusText}`);
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
- const data: AdsResponse = await response.json();
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
- // Debug logging - log the FULL response to see exactly what we're getting
158
- console.log('[AdBanner] Full API Response:', JSON.stringify(data, null, 2));
159
- console.log('[AdBanner] API Response Summary:', {
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
- // Type guard to check if an object is Ad type (for arrays that might contain either)
171
- const isAd = (obj: unknown): obj is Ad => {
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
- // Helper to convert AdData to Ad
177
- const convertToAd = (adData: AdData): Ad => {
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
- // Helper to normalize ad type - "standard" should be treated as "image"
190
- const normalizeAdType = (ad: Ad): Ad => {
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
- // Handle both single ad and array of ads
198
- if (data.ads && Array.isArray(data.ads) && data.ads.length > 0) {
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
- // Handle single ad response - convert AdData to Ad
223
- if (data.ad) {
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
- // No ad found - log detailed info
248
- console.warn('[AdBanner] No ad found in response:', {
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: true,
270
- dev_mode: true,
271
- ad: {
272
- id: 'dev-ad-1',
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
- if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
102
- console.log(`[TheAd Vantage] API Base URL selected: ${selectedUrl} (reason: ${reason})`);
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;