UNPKG

trustlabs-sdk

Version:

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

737 lines (717 loc) 25.8 kB
import { jsx, Fragment } from 'react/jsx-runtime'; import { createContext, useRef, useContext, useEffect, useState } from 'react'; 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); } } 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 }); const TrustBadgeContext = createContext(null); function TrustBadgeProvider({ children }) { const batchQueueRef = useRef({ emails: new Set(), callbacks: [] }); const processBatch = async () => { const { emails, callbacks } = batchQueueRef.current; if (emails.size === 0 || callbacks.length === 0) return; const emailArray = Array.from(emails); console.log(`🚀 TrustBadge: Batching ${emailArray.length} unique emails from ${callbacks.length} components`); try { const results = await requestTrustStatusBatched(emailArray); const resultMap = new Map(results.map(r => [r.email, r])); // Fulfill all pending callbacks callbacks.forEach(({ emails: requestedEmails, onSuccess }) => { const data = requestedEmails .map(email => resultMap.get(email)) .filter((item) => Boolean(item)); onSuccess(data); }); } catch (error) { console.error('🚨 TrustBadge: Batch request failed:', error); callbacks.forEach(({ onError }) => { onError(error); }); } // Reset the batch batchQueueRef.current = { emails: new Set(), callbacks: [] }; }; const requestTrustStatus = (emails, callback, errorCallback) => { const batch = batchQueueRef.current; // Add emails to the batch emails.forEach(email => batch.emails.add(email)); // Add callback batch.callbacks.push({ emails, onSuccess: callback, onError: errorCallback }); // Clear existing timeout and set a new one if (batch.timeoutId) { clearTimeout(batch.timeoutId); } batch.timeoutId = setTimeout(processBatch, 20); // 20ms batch window }; const getTrustStatus = (emails) => { return new Promise((resolve, reject) => { requestTrustStatus(emails, resolve, reject); }); }; return (jsx(TrustBadgeContext.Provider, { value: { getTrustStatus, requestTrustStatus }, children: children })); } function useTrustBadge() { const context = useContext(TrustBadgeContext); if (!context) { throw new Error('useTrustBadge must be used within a TrustBadgeProvider'); } return context; } function useTrustBadgeOptional() { return useContext(TrustBadgeContext); } const TrustBadge = ({ emails, showTooltip = true, onError, onLoad }) => { const trustBadgeContext = useTrustBadgeOptional(); // Inject styles once on mount in client environments useEffect(() => { ensureTrustLabsStylesInjected(); }, []); const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { if (!emails || emails.length === 0) { setError('No emails provided'); setLoading(false); return; } setLoading(true); setError(null); const loadTrustData = async () => { try { let trustData; if (trustBadgeContext) { // Use provider for better batching across components trustData = await trustBadgeContext.getTrustStatus(emails); } else { // Fallback to individual batching with small delay await new Promise(resolve => setTimeout(resolve, 5)); trustData = await requestTrustStatusBatched(emails); } setData(trustData); onLoad?.(trustData); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load trust status'; setError(errorMessage); onError?.(err); } finally { setLoading(false); } }; loadTrustData(); }, [emails, onLoad, onError, trustBadgeContext]); if (loading) { return (jsx("span", { className: "trust-badge loading", children: "Loading..." })); } if (error) { return (jsx("span", { className: "trust-badge error", children: "Error loading badge" })); } if (!data || data.length === 0) { return (jsx("span", { className: "trust-badge", children: "No data available" })); } return (jsx(Fragment, { children: data.map((item) => (jsx("span", { className: `trust-badge ${item.verified ? 'verified' : 'not-verified'}`.trim(), onMouseEnter: (e) => { if (item.completed_at) { showVerificationPopover(e.currentTarget, new Date(item.completed_at).toLocaleDateString()); } }, onMouseLeave: () => { if (item.completed_at) { if (!isPointerCurrentlyOverPopover()) { hideVerificationPopover(); } } }, children: jsx("img", { src: "https://api.trustlabs.pro/static/trustscorebadge.png", alt: item.verified ? 'Verified' : 'Not Verified', style: { height: '16px', width: 'auto', verticalAlign: 'middle', filter: item.verified ? 'none' : 'grayscale(100%) opacity(50%)' } }) }, item.email))) })); }; /** * 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(); /** * Initialize TrustLabs SDK with configuration * @param config Configuration object */ function init(config) { trustLabsSDK.init(config); } /** * Check if SDK is initialized */ function isInitialized() { return trustLabsSDK.isInitialized(); } /** * Auto-detect and configure SDK from common patterns */ function autoInit() { // Try to auto-detect configuration from meta tags or data attributes const endpointMeta = document.querySelector('meta[name="trustlabs-endpoint"]'); const autoRenderMeta = document.querySelector('meta[name="trustlabs-auto-render"]'); let config = {}; if (endpointMeta) { config.endpoint = endpointMeta.getAttribute('content') || undefined; } // Check for data attributes on script tag const scriptTag = document.querySelector('script[data-trustlabs-endpoint]'); if (scriptTag) { const endpoint = scriptTag.getAttribute('data-trustlabs-endpoint'); if (endpoint) { config.endpoint = endpoint; } } // Initialize if we found configuration if (config.endpoint) { init(config); // Auto-render if requested if (autoRenderMeta?.getAttribute('content') === 'true' || scriptTag?.getAttribute('data-trustlabs-auto-render') === 'true') { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { trustLabsSDK.autoRender(); }); } else { trustLabsSDK.autoRender(); } } } } // Auto-initialize if in browser environment if (typeof window !== 'undefined' && typeof document !== 'undefined') { // Try auto-init when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoInit); } else { autoInit(); } } export { TrustBadge, TrustBadge as TrustBadgeComponent, TrustBadgeProvider, TrustLabsError, TrustLabsSDK as default, getTrustStatus, init, isInitialized, renderTrustBadge, renderTrustBadgeWithFetch, setProxy, useTrustBadge }; //# sourceMappingURL=sdk.esm.js.map