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