import { EarnLayerConfig, ConversationOptions, Conversation, DisplayAd, } from './types'; export class EarnLayerClient { private proxyBaseUrl: string; constructor(config: EarnLayerConfig = {}) { this.proxyBaseUrl = config.proxyBaseUrl || '/api/earnlayer'; } async initializeConversation(options?: ConversationOptions): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // Handle deprecated options with warning if (options?.adTypes || options?.frequency) { console.warn( '[EarnLayer SDK] adTypes and frequency options are deprecated. ' + 'Please use adPreferences instead. ' + 'See: https://docs.earnlayerai.com/sdk/ad-preferences' ); } // Build ad_preferences - new way takes priority let ad_preferences: any = {}; if (options?.adPreferences) { // New way: pass through custom adPreferences directly ad_preferences = { ...options.adPreferences }; } else if (options?.adTypes || options?.frequency) { // Old way (deprecated): convert adTypes to ad_types array const ad_types: string[] = []; if (options.adTypes) { // Convert old shortcuts to specific types if (options.adTypes.includes('hyperlink')) { ad_types.push('hyperlink'); } if (options.adTypes.includes('display')) { // 'display' was ambiguous - expand to all display types ad_types.push('thinking', 'banner', 'popup', 'video'); } } if (ad_types.length > 0) { ad_preferences.ad_types = ad_types; } if (options.frequency) { ad_preferences.frequency = options.frequency; } } const requestBody = { visitor_uuid: options?.visitorId || this.generateVisitorId(), ad_preferences: Object.keys(ad_preferences).length > 0 ? ad_preferences : undefined, demo_mode: options?.demoMode ?? false }; try { const response = await fetch(`${this.proxyBaseUrl}/initialize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = `Failed to initialize conversation (${response.status}): ${ errorData.error || errorData.message || response.statusText }`; console.error('[EarnLayer SDK] Failed to initialize conversation:', { endpoint: '/initialize', status: response.status, error: errorData.error || errorData.message || response.statusText, timestamp: new Date().toISOString() }); throw new Error(errorMessage); } return response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { console.error('[EarnLayer SDK] Failed to initialize conversation:', { endpoint: '/initialize', error: 'Request timeout', timestamp: new Date().toISOString() }); } else if (error instanceof Error && !error.message.includes('Failed to initialize conversation')) { console.error('[EarnLayer SDK] Failed to initialize conversation:', { endpoint: '/initialize', error: error.message, timestamp: new Date().toISOString() }); } throw error; } } async getDisplayAd(conversationId: string, adType?: string, thinkingAdTimeout?: number, refresh?: boolean): Promise { if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) { throw new Error('Invalid conversationId: must be a non-empty string'); } const url = new URL(`${this.proxyBaseUrl}/displayad/${conversationId}`, window.location.origin); if (adType) { url.searchParams.append('ad_type', adType); } if (thinkingAdTimeout !== undefined) { url.searchParams.append('thinking_ad_timeout', thinkingAdTimeout.toString()); } if (refresh) { url.searchParams.append('refresh', 'true'); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const response = await fetch(url.toString(), { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { if (response.status === 404) { console.warn('[EarnLayer SDK] No ads available for conversation:', conversationId); return null; } const errorData = await response.json().catch(() => ({})); const errorMessage = `Failed to get display ad (${response.status}): ${ errorData.error || errorData.message || response.statusText }`; console.error('[EarnLayer SDK]', errorMessage, { conversationId, adType, errorDetails: errorData }); throw new Error(errorMessage); } return response.json(); } catch (error) { clearTimeout(timeoutId); throw error; } } async trackImpression(impressionId: string): Promise { let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const response = await fetch(`${this.proxyBaseUrl}/track/impression`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ impression_id: impressionId }), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return true; } catch (error) { attempts++; if (attempts === maxAttempts) { console.error( `[EarnLayer SDK] Failed to track impression after ${maxAttempts} attempts:`, impressionId, error ); return false; } await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); } } return false; } async trackClick(impressionId: string): Promise { let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const response = await fetch(`${this.proxyBaseUrl}/track/click/${impressionId}`, { method: 'GET', signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return true; } catch (error) { attempts++; if (attempts === maxAttempts) { console.error( `[EarnLayer SDK] Failed to track click after ${maxAttempts} attempts:`, impressionId, error ); return false; } await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); } } return false; } async confirmHyperlinkImpressions( conversationId: string, messageText: string ): Promise<{ confirmed_count: number; impression_ids: string[] }> { if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) { const error = new Error('Invalid conversationId: must be a non-empty string'); console.error('[EarnLayer SDK] Failed to confirm hyperlink impressions:', { endpoint: '/impressions/confirm', error: 'Invalid conversationId', timestamp: new Date().toISOString() }); throw error; } if (!messageText || typeof messageText !== 'string' || messageText.trim().length === 0) { const error = new Error('Invalid messageText: must be a non-empty string'); console.error('[EarnLayer SDK] Failed to confirm hyperlink impressions:', { endpoint: '/impressions/confirm', conversationId, error: 'Invalid messageText', timestamp: new Date().toISOString() }); throw error; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const response = await fetch(`${this.proxyBaseUrl}/impressions/confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ conversation_id: conversationId, message_text: messageText, }), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = `Failed to confirm impressions: ${response.statusText}`; console.error('[EarnLayer SDK] Failed to confirm hyperlink impressions:', { endpoint: '/impressions/confirm', conversationId, status: response.status, error: errorData.error || errorData.message || response.statusText, timestamp: new Date().toISOString() }); throw new Error(errorMessage); } return response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { console.error('[EarnLayer SDK] Failed to confirm hyperlink impressions:', { endpoint: '/impressions/confirm', conversationId, error: 'Request timeout', timestamp: new Date().toISOString() }); } else if (error instanceof Error && !error.message.includes('Failed to confirm')) { console.error('[EarnLayer SDK] Failed to confirm hyperlink impressions:', { endpoint: '/impressions/confirm', conversationId, error: error.message, timestamp: new Date().toISOString() }); } throw error; } } private generateVisitorId(): string { return `visitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } }