@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 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.8.0",
3
+ "version": "2.10.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",
@@ -2,7 +2,13 @@
2
2
 
3
3
  import { useEffect, useState } from 'react';
4
4
  import Image from 'next/image';
5
- import { fetchAds, trackImpression, trackClick, type AdData } from '../lib/ads';
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
- const params: Record<string, string> = {};
25
- if (position) {
26
- params.position = position;
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
- const response = await fetchAds(params);
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 - if apiBaseUrl already includes /api/ads, use it directly
128
- // 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
129
132
  let fullApiUrl = apiBaseUrl;
130
133
  if (!fullApiUrl.includes('/api/ads')) {
131
134
  fullApiUrl = fullApiUrl.replace(/\/$/, '') + '/api/ads';
132
135
  }
133
136
 
134
- 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()}`;
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
- const response = await fetch(url, {
141
- method: 'GET',
142
- headers: {
143
- 'Content-Type': 'application/json',
144
- 'Accept': 'application/json',
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
- if (!response.ok) {
149
- throw new Error(`Failed to fetch ad: ${response.status} ${response.statusText}`);
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
- 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
+ }
153
194
 
154
- // Debug logging - log the FULL response to see exactly what we're getting
155
- console.log('[AdBanner] Full API Response:', JSON.stringify(data, null, 2));
156
- 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:', {
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
- // Type guard to check if an object is Ad type (for arrays that might contain either)
168
- 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 => {
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
- // Helper to convert AdData to Ad
174
- const convertToAd = (adData: AdData): Ad => {
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
- // Helper to normalize ad type - "standard" should be treated as "image"
187
- const normalizeAdType = (ad: Ad): Ad => {
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
- // Handle both single ad and array of ads
195
- 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) {
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
- // Handle single ad response - convert AdData to Ad
220
- if (data.ad) {
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
- // No ad found - log detailed info
245
- console.warn('[AdBanner] No ad found in response:', {
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: true,
267
- dev_mode: true,
268
- ad: {
269
- id: 'dev-ad-1',
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
- 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;