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
JavaScript
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