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