UNPKG

trustlabs-sdk

Version:

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

355 lines (339 loc) 11.9 kB
"use client"; import { jsx, Fragment } from 'react/jsx-runtime'; import { createContext, 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 }); } } 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(); }); } 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; } const TrustBadgeContext = createContext(null); 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))) })); }; let customProxy = null; function getProxy() { return customProxy; } var proxy = /*#__PURE__*/Object.freeze({ __proto__: null, getProxy: getProxy }); export { TrustBadge, TrustBadge as default }; //# sourceMappingURL=client.js.map