UNPKG

trustlabs-sdk

Version:

Easy-to-use SDK for displaying trust verification badges on websites. Supports React, Vue, vanilla JS, and CDN usage.

557 lines (539 loc) 20.5 kB
(function (factory) { typeof define === 'function' && define.amd ? define(factory) : factory(); })((function () { 'use strict'; class TrustLabsError extends Error { constructor(message, code, details) { super(message); this.code = code; this.details = details; this.name = 'TrustLabsError'; } } /** * Validates email format */ function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Fetches trust status for a list of email addresses * @param emails Array of email addresses to check * @returns Promise resolving to array of trust status objects */ async function getTrustStatus(emails) { if (!emails || emails.length === 0) { throw new TrustLabsError('At least one email is required', 'INVALID_INPUT', { provided: emails }); } if (emails.length > 100) { throw new TrustLabsError('Maximum 100 emails allowed per request', 'TOO_MANY_EMAILS', { count: emails.length, maximum: 100 }); } // Validate email formats const invalidEmails = emails.filter(email => !isValidEmail(email)); if (invalidEmails.length > 0) { console.warn('TrustLabs SDK: Invalid email formats detected:', invalidEmails); // Filter out invalid emails but continue with valid ones emails = emails.filter(email => isValidEmail(email)); if (emails.length === 0) { throw new TrustLabsError('No valid email addresses provided', 'INVALID_EMAIL_FORMAT', { invalidEmails }); } } // A server proxy is REQUIRED const { getProxy } = await Promise.resolve().then(function () { return proxy; }); const customProxy = getProxy(); if (!customProxy) { throw new TrustLabsError('TrustLabs SDK not configured. Please call init() or setProxy() first.', 'NOT_CONFIGURED', { hint: 'Use TrustLabsSDK.init({ endpoint: "your-api-endpoint" }) or setProxy(proxyFunction)', documentation: 'https://github.com/trustlabs/sdk#setup' }); } try { const results = await customProxy(emails); // Validate response format if (!Array.isArray(results)) { throw new TrustLabsError('Invalid response format from proxy', 'INVALID_RESPONSE', { received: typeof results, expected: 'array' }); } return results; } catch (error) { if (error instanceof TrustLabsError) { throw error; } // Wrap network/proxy errors throw new TrustLabsError(`Failed to fetch trust status: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', { originalError: error, emails }); } } let customProxy = null; function setProxy(proxy) { customProxy = proxy; } function getProxy() { return customProxy; } var proxy = /*#__PURE__*/Object.freeze({ __proto__: null, getProxy: getProxy, setProxy: setProxy }); const STYLE_ELEMENT_ID = 'trustlabs-sdk-styles'; const TRUSTLABS_CSS = ` .trust-badge, .trustlabs-badge { display: inline-block; margin-left: 6px; padding: 2px 6px; font-size: 12px; background: transparent; border-radius: 8px; position: relative; cursor: default; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.2; vertical-align: middle; } /* Tooltip hidden by default; shown on hover */ .trust-badge .tooltip, .trustlabs-badge .trustlabs-tooltip { display: none; position: absolute; top: 120%; left: 0; background: #fff; border: 1px solid #ccc; padding: 4px 8px; font-size: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); white-space: nowrap; border-radius: 4px; z-index: 1000; min-width: 120px; } .trust-badge:hover .tooltip, .trustlabs-badge:hover .trustlabs-tooltip { display: block; } /* Loading state */ .trust-badge.loading, .trustlabs-badge.loading { opacity: 0.6; animation: trustlabs-pulse 1.5s ease-in-out infinite; } @keyframes trustlabs-pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } /* Error state */ .trust-badge.error, .trustlabs-badge.error { background: #fff3e0; color: #ef6c00; font-style: italic; } /* Modal styles */ /* Popover styles */ .trustlabs-popover { position: absolute; background: #fff; color: #111; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 10px; font-size: 12px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); z-index: 10000; white-space: nowrap; } `; function ensureTrustLabsStylesInjected() { if (typeof document === 'undefined') return; if (document.getElementById(STYLE_ELEMENT_ID)) return; const styleEl = document.createElement('style'); styleEl.id = STYLE_ELEMENT_ID; styleEl.type = 'text/css'; styleEl.appendChild(document.createTextNode(TRUSTLABS_CSS)); document.head.appendChild(styleEl); } let popoverElement = null; let isPointerOverPopover = false; function createPopoverIfNeeded() { if (typeof document === 'undefined') return; if (popoverElement) return; popoverElement = document.createElement('div'); popoverElement.className = 'trustlabs-popover'; popoverElement.style.display = 'none'; document.body.appendChild(popoverElement); popoverElement.addEventListener('mouseenter', () => { isPointerOverPopover = true; }); popoverElement.addEventListener('mouseleave', () => { isPointerOverPopover = false; hideVerificationPopover(); }); } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function showVerificationPopover(anchorEl, dateString) { if (typeof document === 'undefined') return; ensureTrustLabsStylesInjected(); createPopoverIfNeeded(); if (!popoverElement) return; popoverElement.textContent = `Verified on ${dateString}`; const rect = anchorEl.getBoundingClientRect(); const scrollX = window.scrollX || window.pageXOffset; const scrollY = window.scrollY || window.pageYOffset; // Preferred position: below and slightly to the right of the badge const padding = 8; const maxWidth = document.documentElement.clientWidth; const tentativeLeft = rect.left + scrollX; const tentativeTop = rect.bottom + scrollY + 6; // Temporarily show to measure width/height popoverElement.style.display = 'block'; popoverElement.style.visibility = 'hidden'; const popW = popoverElement.offsetWidth || 200; popoverElement.offsetHeight || 40; const left = clamp(tentativeLeft, padding, maxWidth - popW - padding); const top = tentativeTop; popoverElement.style.left = `${left}px`; popoverElement.style.top = `${top}px`; popoverElement.style.visibility = 'visible'; } function hideVerificationPopover() { if (!popoverElement) return; popoverElement.style.display = 'none'; } function isPointerCurrentlyOverPopover() { return isPointerOverPopover; } /** * Renders trust badges for email addresses using vanilla JavaScript * @param options Configuration object with target element and trust data */ function renderTrustBadge(options) { const { targetEl, trustData } = options; if (!targetEl) { throw new Error('Target element is required'); } // Ensure styles are injected for vanilla usage ensureTrustLabsStylesInjected(); if (!trustData || trustData.length === 0) { console.warn('No trust data provided to renderTrustBadge'); return; } trustData.forEach((item) => { // Find the email element on the page const emailElements = Array.from(document.querySelectorAll('*')).filter(el => { const text = el.textContent || ''; return text.trim() === item.email; }); if (emailElements.length === 0) { console.warn(`Email element not found on page: ${item.email}`); return; } // Add badge to each found email element emailElements.forEach(emailEl => { // Check if a badge already exists next to this email const nextSibling = emailEl.nextElementSibling; if (nextSibling && nextSibling.classList.contains('trust-badge')) { console.log(`Badge already exists for email: ${item.email}`); return; // Skip adding another badge } const badge = document.createElement('span'); badge.className = `trust-badge ${item.verified ? 'verified' : 'not-verified'}`.trim(); // Set badge content - always use the image const img = document.createElement('img'); img.src = 'https://api.trustlabs.pro/static/trustscorebadge.png'; img.alt = item.verified ? 'Verified' : 'Not Verified'; img.style.height = '16px'; img.style.width = 'auto'; img.style.verticalAlign = 'middle'; // Apply gray filter for unverified users if (!item.verified) { img.style.filter = 'grayscale(100%) opacity(50%)'; } badge.appendChild(img); // Modal hover interactions when completion date is available if (item.completed_at) { const dateString = new Date(item.completed_at).toLocaleDateString(); badge.addEventListener('mouseenter', () => { showVerificationPopover(badge, dateString); }); badge.addEventListener('mouseleave', () => { if (!isPointerCurrentlyOverPopover()) { hideVerificationPopover(); } }); } // Insert badge after the email element emailEl.insertAdjacentElement('afterend', badge); }); }); } /** * Renders trust badges with automatic data fetching * @param options Configuration object with target element and emails */ async function renderTrustBadgeWithFetch(options) { const { targetEl, emails } = options; try { // Show loading state const loadingBadge = document.createElement('span'); loadingBadge.className = 'trust-badge loading'; loadingBadge.textContent = 'Loading...'; targetEl.insertAdjacentElement('afterend', loadingBadge); // Import and call batched request const { requestTrustStatusBatched } = await Promise.resolve().then(function () { return batch; }); const trustData = await requestTrustStatusBatched(emails); // Remove loading badge loadingBadge.remove(); // Render actual badges renderTrustBadge({ targetEl, trustData }); } catch (error) { console.error('Error rendering trust badge:', error); // Show error state const errorBadge = document.createElement('span'); errorBadge.className = 'trust-badge error'; errorBadge.textContent = 'Error loading badge'; targetEl.insertAdjacentElement('afterend', errorBadge); } } /** * Main TrustLabs SDK class providing a simplified API for third-party developers */ class TrustLabsSDK { constructor() { this.config = { autoInjectStyles: true }; this.initialized = false; } /** * Initialize the SDK with configuration */ init(config) { this.config = { ...this.config, ...config }; if (this.config.autoInjectStyles !== false) { ensureTrustLabsStylesInjected(); } // Set up proxy based on config if (this.config.proxy) { setProxy(this.config.proxy); } else if (this.config.endpoint) { // Create default proxy using provided endpoint const endpoint = this.config.endpoint; setProxy(async (emails) => { // Try POST first, fallback to GET if 405 Method Not Allowed let response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ emails }), }); // If POST not allowed, try GET with query parameters if (response.status === 405) { console.log('POST not supported, trying GET with query parameters'); const params = emails.map(encodeURIComponent).join(','); const getUrl = `${endpoint}?emails=${params}`; response = await fetch(getUrl, { method: 'GET', }); } if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.results || data; }); } this.initialized = true; } /** * Check if SDK is initialized */ isInitialized() { return this.initialized; } /** * Get trust status for emails */ async getTrustStatus(emails) { if (!this.initialized) { throw new Error('TrustLabsSDK not initialized. Call init() first.'); } return getTrustStatus(emails); } /** * Render trust badge with existing data */ renderBadge(options) { if (!this.initialized) { throw new Error('TrustLabsSDK not initialized. Call init() first.'); } const mergedOptions = { ...this.config.defaultOptions, ...options }; renderTrustBadge(mergedOptions); } /** * Render trust badge with automatic data fetching */ async renderBadgeWithFetch(options) { if (!this.initialized) { throw new Error('TrustLabsSDK not initialized. Call init() first.'); } return renderTrustBadgeWithFetch(options); } /** * Auto-detect and render badges for all email addresses on the page */ async autoRender(selector = '.trust-email') { if (!this.initialized) { throw new Error('TrustLabsSDK not initialized. Call init() first.'); } const emailElements = document.querySelectorAll(selector); for (const element of emailElements) { const email = element.textContent?.trim(); if (email && this.isValidEmail(email)) { try { await this.renderBadgeWithFetch({ targetEl: element, emails: [email] }); } catch (error) { console.warn(`Failed to render badge for ${email}:`, error); } } } } /** * Utility to validate email format */ isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Update configuration after initialization */ configure(config) { this.config = { ...this.config, ...config }; if (config.proxy) { setProxy(config.proxy); } else if (config.endpoint) { const endpoint = config.endpoint; setProxy(async (emails) => { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ emails }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.results || data; }); } } /** * Get current configuration */ getConfig() { return { ...this.config }; } } // Export a default instance for easy usage const trustLabsSDK = new TrustLabsSDK(); // Legacy support - expose static methods const init = (config) => trustLabsSDK.init(config); const isInitialized = () => trustLabsSDK.isInitialized(); const autoRender = (selector) => trustLabsSDK.autoRender(selector); // Browser-specific entry point window.TrustLabsSDK = { // Legacy API for backward compatibility getTrustStatus, setProxy, renderTrustBadge, renderTrustBadgeWithFetch, // New simplified API init, isInitialized, autoRender, sdk: trustLabsSDK }; const emailCache = new Map(); let pendingEmails = new Set(); let pendingRequests = []; let scheduled = false; function scheduleFlush() { if (scheduled) return; scheduled = true; // Use a small delay to allow multiple components to batch together setTimeout(flushBatch, 10); } async function flushBatch() { scheduled = false; const emailsToFetch = Array.from(pendingEmails); const requests = pendingRequests; pendingEmails = new Set(); pendingRequests = []; try { const results = emailsToFetch.length > 0 ? await getTrustStatus(emailsToFetch) : []; for (const item of results) { emailCache.set(item.email, item); } // Fulfill each request using cached + fresh results for (const req of requests) { const subset = req.emails .map((e) => emailCache.get(e)) .filter((v) => Boolean(v)); req.resolve(subset); } } catch (err) { for (const req of requests) req.reject(err); } } function requestTrustStatusBatched(emails) { const unique = Array.from(new Set(emails)); // Check cache first const cached = []; const toQueue = []; for (const e of unique) { const hit = emailCache.get(e); if (hit) cached.push(hit); else toQueue.push(e); } return new Promise((resolve, reject) => { if (toQueue.length === 0) { resolve(cached); return; } for (const e of toQueue) pendingEmails.add(e); pendingRequests.push({ emails: unique, resolve, reject }); scheduleFlush(); }); } var batch = /*#__PURE__*/Object.freeze({ __proto__: null, requestTrustStatusBatched: requestTrustStatusBatched }); })); //# sourceMappingURL=sdk.min.js.map