@vitalwall/client
Version:
VitalWall client library for vanilla JavaScript applications
989 lines (970 loc) • 39 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
// Browser environment detection
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
class VitalWall {
constructor() {
this.apiKey = '';
this.baseUrl = '';
this.domain = '';
this.projectId = '';
this.debug = false;
this.initialized = false;
this.initializationPromise = null;
this.currentWallId = null;
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiryTime = null;
this.subscription = null;
this.initializedWalls = new Set();
this.mutationObserver = null;
this.readyCallbacks = [];
// Track current items and config per wall for incremental updates
this.wallData = new Map();
}
init(config) {
return __awaiter(this, void 0, void 0, function* () {
if (!config.apiKey) {
this.error('VitalWall: apiKey is required');
throw new Error('VitalWall: apiKey is required');
}
this.apiKey = config.apiKey;
this.baseUrl = 'https://clyfgobyllqtvmfchczj.supabase.co/functions/v1';
// Auto-detect domain in browser, require explicit domain in Node.js for security
if (isBrowser) {
this.domain = window.location.hostname;
}
else {
if (!config.domain) {
this.error('VitalWall: domain is required when running in Node.js environment');
throw new Error('VitalWall: domain is required when running in Node.js environment');
}
this.domain = config.domain;
}
this.projectId = this.baseUrl.split('/')[2].split('.')[0];
this.debug = config.debug || false;
this.currentWallId = config.wallId || null;
this.log('VitalWall initialized with domain:', this.domain);
// Try to load existing token (browser only)
if (isBrowser) {
const localStorage = window.localStorage;
const storedToken = localStorage.getItem('vitalwall-token');
if (storedToken) {
try {
const tokenData = JSON.parse(storedToken);
if (tokenData.domain === this.domain && Date.now() < tokenData.expiryTime) {
this.log('Using stored token');
this.accessToken = tokenData.accessToken;
this.refreshToken = tokenData.refreshToken;
this.tokenExpiryTime = tokenData.expiryTime;
if (!this.currentWallId && tokenData.wallId) {
this.currentWallId = tokenData.wallId;
}
this.scheduleTokenRefresh();
this.initialized = true;
this.setupWallObserver();
this.setupTracking();
this.dispatchReadyEvent();
this.checkForWalls();
return;
}
}
catch (e) {
this.error('Error parsing stored token:', e);
}
}
}
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiryTime = null;
// Initialize tokens and setup
this.initializationPromise = this.initializeTokens().then(() => {
if (isBrowser) {
this.setupWallObserver();
this.setupTracking();
this.checkForWalls();
}
this.initialized = true;
this.dispatchReadyEvent();
});
return this.initializationPromise;
});
}
addItem(item) {
return __awaiter(this, void 0, void 0, function* () {
const wallId = item.wallId || this.currentWallId;
if (!wallId) {
this.error('No wall ID specified and no current wall set');
return undefined;
}
return new Promise((resolve, reject) => {
this.sendUpdate(item.content, item.isHtml || false, wallId, (itemId) => {
resolve(itemId);
}, (error) => {
reject(error);
});
});
});
}
addTextItem(content, wallId) {
return __awaiter(this, void 0, void 0, function* () {
return this.addItem({ content, isHtml: false, wallId });
});
}
addHtmlItem(content, wallId) {
return __awaiter(this, void 0, void 0, function* () {
return this.addItem({ content, isHtml: true, wallId });
});
}
setCurrentWall(wallId) {
this.currentWallId = wallId;
// Update stored token with new wall ID (browser only)
if (isBrowser) {
const storedToken = localStorage.getItem('vitalwall-token');
if (storedToken) {
try {
const tokenData = JSON.parse(storedToken);
tokenData.wallId = wallId;
localStorage.setItem('vitalwall-token', JSON.stringify(tokenData));
}
catch (e) {
this.error('Error updating stored token with wall ID:', e);
}
}
}
}
getCurrentWall() {
return this.currentWallId;
}
isInitialized() {
return this.initialized;
}
onReady(callback) {
if (this.initialized) {
callback();
}
else {
this.readyCallbacks.push(callback);
}
}
cleanup() {
this.cleanupAllWalls();
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
this.initialized = false;
this.initializationPromise = null;
}
log(...args) {
if (this.debug) {
console.log('[VitalWall]', ...args);
}
}
error(...args) {
if (this.debug) {
console.error('[VitalWall]', ...args);
}
}
dispatchReadyEvent() {
if (isBrowser) {
window.dispatchEvent(new Event('vitalwall-loaded'));
}
this.readyCallbacks.forEach(callback => callback());
this.readyCallbacks = [];
}
checkForWalls() {
if (!isBrowser)
return;
const maxAttempts = 20; // 10 seconds total
let attempts = 0;
const check = () => {
if (attempts >= maxAttempts)
return;
attempts++;
const walls = document.querySelectorAll('[data-vital-wall]:not([data-vital-wall-initialized])');
if (walls.length > 0) {
this.initializeWalls();
return;
}
// Schedule next check
setTimeout(check, 500);
};
// Start checking
check();
}
initializeWalls() {
if (!isBrowser)
return;
const wallElements = document.querySelectorAll('[data-vital-wall]:not([data-vital-wall-initialized])');
wallElements.forEach(element => this.initializeWall(element));
}
setupWallObserver() {
if (!isBrowser || !document.body)
return;
this.mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.hasAttribute('data-vital-wall')) {
this.initializeWall(element);
}
// Check for child elements with data-vital-wall
const children = element.querySelectorAll('[data-vital-wall]');
children.forEach(child => this.initializeWall(child));
}
});
});
});
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
initializeWall(element) {
if (!isBrowser || !element || element.hasAttribute('data-vital-wall-initialized')) {
return;
}
// Get wall ID from data attribute
const wallId = element.getAttribute('data-vital-wall');
if (!wallId) {
this.error('data-vital-wall attribute must contain a wall ID');
return;
}
if (!this.currentWallId) {
this.currentWallId = wallId;
}
// Store the wall ID with the token data (browser only)
if (isBrowser) {
const localStorage = window.localStorage;
const storedToken = localStorage.getItem('vitalwall-token');
if (storedToken) {
try {
const tokenData = JSON.parse(storedToken);
if (!tokenData.wallId) {
tokenData.wallId = wallId;
localStorage.setItem('vitalwall-token', JSON.stringify(tokenData));
}
}
catch (e) {
this.error('Error updating stored token:', e);
}
}
}
// Style the element and create container
this.styleWallElement(element);
// Create container
const container = document.createElement('div');
container.className = 'vital-wall-container';
container.style.cssText = `
width: 100%;
max-width: 100%;
box-sizing: border-box;
position: relative;
overflow: auto;
`;
// Add loading state
container.innerHTML = '<div class="wall-item">Loading...</div>';
// Clear element and add container
element.innerHTML = '';
element.appendChild(container);
// Add base styles
this.addBaseStyles();
// Store cleanup function
element._cleanup = () => {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
};
// Add mutation observer to detect when element is removed
if (isBrowser && element.parentNode) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node === element && element._cleanup) {
element._cleanup();
}
});
});
});
observer.observe(element.parentNode, {
childList: true
});
}
// Load items and setup WebSocket
this.loadItems(wallId, container);
this.setupWebSocket(wallId, container);
// Mark as initialized
element.setAttribute('data-vital-wall-initialized', 'true');
this.initializedWalls.add(element);
}
styleWallElement(element) {
element.style.cssText = `
display: block;
width: 100%;
max-width: 100%;
overflow: auto;
box-sizing: border-box;
position: relative;
`;
}
addBaseStyles() {
if (!isBrowser)
return;
// Check if styles already added
if (document.getElementById('vitalwall-styles'))
return;
const style = document.createElement('style');
style.id = 'vitalwall-styles';
style.textContent = `
.vital-wall-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
position: relative;
}
.wall-item {
background: white;
border-radius: 4px;
padding: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
/* List layout specific styles */
.vital-wall-container.list-layout {
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
height: 100%;
}
/* Grid layout specific styles */
.vital-wall-container.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
padding: 1rem;
}
.vital-wall-container.grid-layout .wall-item {
height: 100%;
}
/* Ticker-specific styles */
.ticker-viewport {
position: relative;
width: 100%;
overflow: hidden;
}
.ticker-scroll {
display: flex;
flex-direction: row-reverse;
gap: 1rem;
padding: 1rem;
overflow-x: auto;
}
.ticker-scroll .wall-item {
flex: 0 0 auto;
min-width: 200px;
max-width: 300px;
}
.vital-status-container {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
padding: 8px;
margin-top: 8px;
}
`;
document.head.appendChild(style);
}
applyCustomCss(css, wallId) {
if (!isBrowser || !css)
return;
const styleId = `vitalwall-custom-${wallId}`;
let styleElement = document.getElementById(styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
// Scope CSS to this specific wall
const scopedCss = css.replace(/([^{}]+)\{/g, (match, selector) => {
const selectors = selector.split(',').map((s) => s.trim());
const scopedSelectors = selectors.map((s) => {
if (s.startsWith('@') || s === ':root' || s.includes(`[data-vital-wall="${wallId}"]`)) {
return s;
}
return `[data-vital-wall="${wallId}"] ${s}`;
});
return `${scopedSelectors.join(', ')}{`;
});
styleElement.textContent = scopedCss;
}
loadItems(wallId, container) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.accessToken) {
this.error('No access token available');
return;
}
try {
const response = yield fetch(`${this.baseUrl}/wall-items?wall_id=${wallId}`, {
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Origin': isBrowser ? window.location.origin : this.domain
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = yield response.json();
if (data.error)
throw new Error(data.error);
this.renderItems(data, container, wallId);
}
catch (error) {
this.error('Error loading items:', error);
container.innerHTML = '<div class="wall-item">Error loading wall data</div>';
}
});
}
setupWebSocket(wallId, container) {
if (!isBrowser)
return;
// Use the anon key for initial connection, but set up authenticated session
const anonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNseWZnb2J5bGxxdHZtZmNoY3pqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjUxNDU3MDYsImV4cCI6MjA0MDcyMTcwNn0.yODfQfQaKeHy3c17uKfJx_wED8cwJ4TK-R4b0qoZ7pM';
const initializeSupabase = () => __awaiter(this, void 0, void 0, function* () {
var _a;
if ((_a = window.supabase) === null || _a === void 0 ? void 0 : _a.createClient) {
// Create client with anon key first
const client = window.supabase.createClient(`https://${this.projectId}.supabase.co`, anonKey);
// Set custom JWT for realtime subscriptions
// This allows RLS policies to work with auth.jwt()->>'wallId'
// Use realtime.setAuth() for custom JWTs (not auth.setSession which is for Supabase Auth)
if (this.accessToken) {
try {
client.realtime.setAuth(this.accessToken);
this.log('Custom JWT set for realtime subscriptions');
}
catch (error) {
this.error('Failed to set realtime auth:', error);
}
}
window.supabase = client;
this.setupRealtimeSubscription(wallId, container);
}
});
// Load Supabase client if not already loaded
if (!window.supabase) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2';
script.onload = () => {
initializeSupabase();
};
document.head.appendChild(script);
}
else {
initializeSupabase();
}
}
setupRealtimeSubscription(wallId, container) {
if (!isBrowser)
return;
this.log('Setting up realtime subscription for wall:', wallId);
const channelName = `wall:${wallId}`;
// Use broadcast instead of postgres_changes to bypass RLS requirements
// The server broadcasts new items after inserting them
const subscription = window.supabase
.channel(channelName)
.on('broadcast', { event: 'new_item' }, (payload) => {
var _a;
this.log('Received broadcast:', payload);
// Add the new item with animation instead of reloading everything
// Try different payload structures - Supabase broadcast structure can vary
const newItem = ((_a = payload === null || payload === void 0 ? void 0 : payload.payload) === null || _a === void 0 ? void 0 : _a.new) || (payload === null || payload === void 0 ? void 0 : payload.new) || (payload === null || payload === void 0 ? void 0 : payload.payload);
this.log('Extracted new item:', newItem);
if (newItem && newItem.content) {
this.addNewItemWithAnimation(wallId, newItem, container);
}
else {
// Fallback to full reload if payload structure is unexpected
this.log('Could not extract new item, falling back to full reload');
this.loadItems(wallId, container);
}
})
.subscribe((status) => {
this.log('Subscription status:', status);
});
this.subscription = subscription;
}
addNewItemWithAnimation(wallId, newItem, container) {
var _a;
if (!isBrowser)
return;
const wallData = this.wallData.get(wallId);
if (!wallData) {
// No wall data yet, do a full load
this.loadItems(wallId, container);
return;
}
const { items, config } = wallData;
// Check for duplicates if distinct_items_only is enabled
if (config.distinct_items_only) {
const normalizedNewContent = ((_a = newItem.content) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase()) || '';
const existingIndex = items.findIndex(item => { var _a; return (((_a = item.content) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase()) || '') === normalizedNewContent; });
if (existingIndex !== -1) {
this.log('Duplicate found at index', existingIndex, '- moving to top');
// Remove the existing item from its current position
items.splice(existingIndex, 1);
// Also remove from DOM - find and remove the element
// For ticker layout, items are inside .ticker-scroll
const itemContainer = config.layout === 'ticker'
? container.querySelector('.ticker-scroll') || container
: container;
const existingElements = itemContainer.querySelectorAll('.wall-item');
if (existingElements[existingIndex]) {
existingElements[existingIndex].remove();
}
}
}
// Add the new item to our tracked data at the beginning
items.unshift(newItem);
// Enforce display limit
const displayLimit = config.display_limit || 10;
while (items.length > displayLimit) {
items.pop();
}
// Update stored data
this.wallData.set(wallId, { items, config });
// Create the new item element with animation
const itemElement = document.createElement('div');
itemElement.className = 'wall-item wall-item-new';
if (newItem.is_html) {
itemElement.innerHTML = newItem.content;
}
else {
itemElement.innerHTML = `<p>${newItem.content}</p>`;
}
// Add animation styles
itemElement.style.cssText = `
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
`;
// Find the right container based on layout
if (config.layout === 'ticker') {
const tickerScroll = container.querySelector('.ticker-scroll');
if (tickerScroll) {
// For ticker, insert at the beginning (first child)
tickerScroll.insertBefore(itemElement, tickerScroll.firstChild);
}
}
else {
// For list/grid layouts, insert at the beginning of container
container.insertBefore(itemElement, container.firstChild);
}
// Trigger animation after element is in DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
itemElement.style.opacity = '1';
itemElement.style.transform = 'translateY(0)';
});
});
// Remove excess items from DOM if over limit
if (config.layout !== 'ticker') {
const allItems = container.querySelectorAll('.wall-item');
container.querySelector('.vital-status-container');
// Count items excluding status container
let itemCount = 0;
allItems.forEach(item => {
if (!item.classList.contains('vital-status-container')) {
itemCount++;
if (itemCount > displayLimit) {
item.remove();
}
}
});
}
else {
const tickerScroll = container.querySelector('.ticker-scroll');
if (tickerScroll) {
const tickerItems = tickerScroll.querySelectorAll('.wall-item');
if (tickerItems.length > displayLimit) {
// Remove from the beginning for ticker (oldest items)
tickerItems[0].remove();
}
}
}
// Clean up animation class after animation completes
setTimeout(() => {
itemElement.classList.remove('wall-item-new');
itemElement.style.transition = '';
}, 350);
}
renderItems(data, container, wallId) {
const { items, config } = data;
this.log('Rendering items:', (items === null || items === void 0 ? void 0 : items.length) || 0);
// Store the wall data for incremental updates
this.wallData.set(wallId, { items: items || [], config });
if (!items || items.length === 0) {
container.innerHTML = '<div class="wall-item">No items to display</div>';
return;
}
container.className = `vital-wall-container ${config.layout || 'list'}-layout`;
// Apply custom CSS if provided
if (config.custom_css) {
this.applyCustomCss(config.custom_css, wallId);
}
if (config.layout === 'ticker') {
this.renderTickerLayout(items, container);
}
else {
this.renderStandardLayout(items, container);
}
this.addStatusContainer(container);
}
renderTickerLayout(items, container) {
if (!isBrowser)
return;
const viewport = document.createElement('div');
viewport.className = 'ticker-viewport';
const tickerScroll = document.createElement('div');
tickerScroll.className = 'ticker-scroll';
items.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'wall-item';
if (item.is_html) {
itemElement.innerHTML = item.content;
}
else {
itemElement.innerHTML = `<p>${item.content}</p>`;
}
tickerScroll.appendChild(itemElement);
});
viewport.appendChild(tickerScroll);
container.innerHTML = '';
container.appendChild(viewport);
}
renderStandardLayout(items, container) {
if (!isBrowser)
return;
container.innerHTML = '';
items.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'wall-item';
if (item.is_html) {
itemElement.innerHTML = item.content;
}
else {
itemElement.innerHTML = `<p>${item.content}</p>`;
}
container.appendChild(itemElement);
});
}
addStatusContainer(container) {
if (!isBrowser)
return;
const statusContainer = document.createElement('div');
statusContainer.className = 'vital-status-container';
const connectionStatus = document.createElement('div');
connectionStatus.className = 'vital-connection';
connectionStatus.innerHTML = `
<span class="connection-dot" style="
display: inline-block;
width: 8px;
height: 8px;
background-color: #4CAF50;
border-radius: 50%;
margin-right: 4px;
"></span>
<span>Connected</span>
`;
const poweredBy = document.createElement('div');
poweredBy.className = 'vital-powered';
poweredBy.innerHTML = 'Powered by <span style="color: rgb(237, 28, 36)">Vital</span><span style="color: rgb(51, 51, 51); font-family: Cal Sans, sans-serif">Wall</span>';
poweredBy.style.cssText = `
font-weight: 500;
cursor: pointer;
`;
poweredBy.onclick = () => {
// Track the click
this.trackPoweredByClick();
// Open with UTM params
const url = new URL('https://vitalwall.com');
url.searchParams.set('utm_source', 'embed');
url.searchParams.set('utm_medium', 'powered_by');
url.searchParams.set('utm_campaign', this.domain);
url.searchParams.set('utm_content', this.currentWallId || 'unknown');
window.open(url.toString(), '_blank');
};
statusContainer.appendChild(connectionStatus);
statusContainer.appendChild(poweredBy);
container.appendChild(statusContainer);
}
trackPoweredByClick() {
// Fire and forget - don't block the link opening
fetch(`${this.baseUrl}/track-powered-by-click`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
wall_id: this.currentWallId,
domain: this.domain,
timestamp: new Date().toISOString()
})
}).catch(() => {
// Silently ignore errors - tracking shouldn't affect UX
});
}
initializeTokens() {
return __awaiter(this, void 0, void 0, function* () {
this.log('Initializing tokens...');
try {
const response = yield fetch(`${this.baseUrl}/generate-wall-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
apiKey: this.apiKey,
domain: this.domain
})
});
this.log('Token response status:', response.status);
const data = yield response.json();
this.log('Token response data:', data);
if (data.error)
throw new Error(data.error);
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
this.tokenExpiryTime = Date.now() + (data.expires_in * 1000);
this.log('Tokens initialized successfully');
// Store token data (browser only)
if (isBrowser) {
const tokenData = {
accessToken: this.accessToken,
refreshToken: this.refreshToken,
expiryTime: this.tokenExpiryTime,
domain: this.domain,
wallId: this.currentWallId || undefined
};
localStorage.setItem('vitalwall-token', JSON.stringify(tokenData));
}
this.scheduleTokenRefresh();
}
catch (error) {
this.error('Error initializing tokens:', error);
throw error;
}
});
}
scheduleTokenRefresh() {
if (!this.tokenExpiryTime)
return;
const REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes
const timeUntilRefresh = this.tokenExpiryTime - Date.now() - REFRESH_BUFFER;
setTimeout(() => __awaiter(this, void 0, void 0, function* () {
try {
yield this.refreshAccessToken();
}
catch (error) {
this.error('Error refreshing token:', error);
}
}), Math.max(0, timeUntilRefresh));
}
refreshAccessToken() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = yield fetch(`${this.baseUrl}/refresh-wall-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refresh_token: this.refreshToken,
domain: this.domain
})
});
const data = yield response.json();
if (data.error)
throw new Error(data.error);
this.accessToken = data.access_token;
this.tokenExpiryTime = Date.now() + (data.expires_in * 1000);
// Update stored token data (browser only)
if (isBrowser) {
const tokenData = {
accessToken: this.accessToken,
refreshToken: this.refreshToken,
expiryTime: this.tokenExpiryTime,
domain: this.domain,
wallId: this.currentWallId || undefined
};
localStorage.setItem('vitalwall-token', JSON.stringify(tokenData));
}
this.scheduleTokenRefresh();
});
}
setupTracking() {
if (!isBrowser)
return;
// Track existing elements
const vitalItems = document.querySelectorAll('[data-vital-item]');
vitalItems.forEach(element => this.trackVitalItem(element));
// Set up observer for dynamically added elements
this.setupTrackingMutationObserver();
}
setupTrackingMutationObserver() {
if (!isBrowser || !document.body)
return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.hasAttribute('data-vital-item')) {
this.trackVitalItem(element);
}
const children = element.querySelectorAll('[data-vital-item]');
children.forEach(child => this.trackVitalItem(child));
}
});
});
});
try {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
catch (error) {
this.error('Error setting up tracking observer:', error);
}
}
trackVitalItem(element) {
if (!isBrowser)
return;
let content = element.getAttribute('data-vital-item') || '';
const isHtml = element.getAttribute('data-vital-html') === 'true';
const isAuto = element.getAttribute('data-vital-auto') === 'true';
const sendContentId = element.getAttribute('data-vital-send-content');
// If sendContentId is provided, find and use that element's content
if (sendContentId) {
const targetElement = document.getElementById(sendContentId);
if (targetElement) {
content = isHtml ? targetElement.innerHTML : targetElement.textContent || '';
}
}
// If no content specified, use the element's HTML or text
if (!content) {
content = isHtml ? element.innerHTML : element.textContent || '';
}
// Don't track empty content
if (!content)
return;
// Send update immediately if auto-tracking is enabled
if (isAuto || (!element.getAttribute('data-vital-item') && !sendContentId)) {
this.sendUpdate(content, isHtml);
return;
}
// Otherwise, track click events
element.addEventListener('click', () => {
this.sendUpdate(content, isHtml);
});
}
sendUpdate(content, isHtml, wallId, onSuccess, onError) {
const targetWallId = wallId || this.currentWallId;
if (!targetWallId) {
const error = new Error('No wall ID specified');
if (onError)
onError(error);
return;
}
// For real authentication, we need Bearer token
if (!this.accessToken) {
const error = new Error('No access token available. VitalWall may not be properly initialized.');
this.error('sendUpdate failed:', error.message);
if (onError)
onError(error);
return;
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`
};
const requestBody = {
content: content,
isHtml: isHtml,
wall_id: targetWallId,
domain: this.domain
};
this.log('Sending request to:', `${this.baseUrl}/wall-update`);
this.log('Request headers:', headers);
this.log('Request body:', requestBody);
fetch(`${this.baseUrl}/wall-update`, {
method: 'POST',
headers: headers,
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
if (data.error)
throw new Error(data.error);
if (onSuccess)
onSuccess(data.id);
})
.catch(error => {
this.error('Error sending update:', error);
if (onError)
onError(error);
});
}
cleanupAllWalls() {
if (this.initializedWalls) {
this.initializedWalls.forEach(element => {
if (element._cleanup) {
element._cleanup();
}
element.removeAttribute('data-vital-wall-initialized');
});
this.initializedWalls.clear();
}
}
}
// For convenient instantiation
const createVitalWall = () => new VitalWall();
exports.createVitalWall = createVitalWall;
exports.default = VitalWall;