const LOCAL_FALLBACK_PROJECTS = [ { provider: 'giveth', slug: 'Save-the-Children-Philippines', name: 'Save the Children Philippines', donateUrl: 'https://giveth.io/project/Save-the-Children-Philippines#donate', network: 'Polygon', currency: 'POL' }, { provider: 'giveth', slug: 'Save-the-Children-International', name: 'Save the Children International', donateUrl: 'https://giveth.io/project/Save-the-Children-International#donate', network: 'Polygon', currency: 'POL' }, { provider: 'giveth', slug: 'scholarship-programme-for-underprivileged-children', name: 'Scholarship Programme For Underprivileged Children', donateUrl: 'https://giveth.io/project/scholarship-programme-for-underprivileged-children#donate', network: 'Polygon', currency: 'POL', recipientAddress: '0x37aB975aB79eAB2d6849e28339E33963143aae6E' } ]; const MAX_SOULWALL_CARDS = 3; const DEPLOYMENT_LABELS = { 'soulwall-org': 'SoulWall.org', 'livenewsindex-soulwall': 'Live News Index', 'livenewsindex-com': 'Live News Index', 'enhancemarkets-soulwall': 'EnhanceMarkets Dashboard' }; const state = { provider: 'giveth', projects: [], selected: null, hoveredHeroCardKey: '', intent: null, session: { access: false, expiresAt: '', secondsRemaining: 0 } }; const els = { grid: document.querySelector('#project-grid'), heroGrid: document.querySelector('#hero-project-grid'), refresh: document.querySelector('#refresh-projects'), gatewayGuide: document.querySelector('.charity-gateway-guide'), helperPanel: document.querySelector('.charity-wallet-helper'), proofForm: document.querySelector('#proof-form'), txHash: document.querySelector('#tx-hash'), verifyBtn: document.querySelector('#verify-btn'), menuToggle: document.querySelector('#menu-toggle'), siteNav: document.querySelector('#site-nav'), minDonationLabel: document.querySelector('#min-donation-label'), ledgerRefresh: document.querySelector('#refresh-ledger'), ledgerTotalUsd: document.querySelector('#ledger-total-usd'), ledgerDonationCount: document.querySelector('#ledger-donation-count'), ledgerProjectCount: document.querySelector('#ledger-project-count'), ledgerIntegrationCount: document.querySelector('#ledger-integration-count'), ledgerList: document.querySelector('#ledger-list'), impactConfetti: document.querySelector('#impact-confetti-link'), providerLink: document.querySelector('#charity-provider-link'), helperLabel: document.querySelector('#charity-proof-helper-label'), helperCopy: document.querySelector('#charity-proof-helper-copy'), routeBox: document.querySelector('#charity-route-box'), routeLabel: document.querySelector('#charity-route-label'), routeAddress: document.querySelector('#charity-route-address'), routeCopy: document.querySelector('#charity-route-copy'), deviceFlowNote: document.querySelector('#charity-device-flow-note'), txHashLabel: document.querySelector('#tx-hash-label'), selectedTitle: document.querySelector('#charity-selected-title'), verifyStatus: document.querySelector('#change-checkout-status'), stepItems: Array.from(document.querySelectorAll('#charity-step-list li')), accessText: document.querySelector('#soulwall-access-text'), windowText: document.querySelector('#soulwall-window-text'), heroLedgerTotal: document.querySelector('#hero-ledger-total'), heroLedgerCaption: document.querySelector('#hero-ledger-caption'), heroLedgerDonations: document.querySelector('#hero-ledger-donations'), heroLedgerIntegrations: document.querySelector('#hero-ledger-integrations'), heroLedgerProjects: document.querySelector('#hero-ledger-projects') }; const SOULWALL_WARNING_PATTERN = /SOULWALL_KV|SOULWALL_ALLOW_INSECURE_PREVIEW|donation intents/i; function getMetaContent(name) { return document.querySelector(`meta[name="${name}"]`)?.getAttribute('content')?.trim() || ''; } function hostToClientId(value) { return String(value || '') .trim() .toLowerCase() .replace(/^https?:\/\//, '') .replace(/^www\./, '') .replace(/[:/].*$/, '') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'soulwall-deployment'; } const deploymentConfig = window.SOULWALL_DEPLOYMENT || {}; const deploymentMeta = { clientId: deploymentConfig.clientId || getMetaContent('soulwall:client-id') || hostToClientId(window.location.hostname), deploymentTitle: deploymentConfig.deploymentTitle || getMetaContent('soulwall:deployment-title') || document.title || '', deploymentDescription: deploymentConfig.deploymentDescription || getMetaContent('soulwall:deployment-description') || getMetaContent('description') || '', sourceWebsite: deploymentConfig.sourceWebsite || getMetaContent('soulwall:source-website') || window.location.origin || '', integrationOrigin: deploymentConfig.integrationOrigin || window.location.origin || '' }; function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll('\'', '''); } function sanitizeSoulwallMessage(message) { const text = String(message || '').trim(); if (!text) return ''; if (SOULWALL_WARNING_PATTERN.test(text)) { return 'SoulWall is ready. Open the selected charity page, then return with the confirmed transaction hash to continue.'; } return text; } async function fetchJson(url, options) { const response = await fetch(url, options); const payload = await response.json().catch(() => ({})); if (!response.ok && response.status !== 202) { const message = sanitizeSoulwallMessage(payload.message || payload.error || `Request failed: ${response.status}`); throw new Error(message); } return payload; } function forceAdvanceToNextStep() { const target = els.helperPanel || els.gatewayGuide || els.proofForm || els.txHash; if (!target) return; target.scrollIntoView({ behavior: 'smooth', block: 'start' }); window.setTimeout(() => { if (els.providerLink && els.providerLink.getAttribute('aria-disabled') !== 'true') { els.providerLink.focus({ preventScroll: true }); return; } if (els.txHash) { els.txHash.focus({ preventScroll: true }); } }, 240); } function setupMobileMenu() { if (!els.menuToggle || !els.siteNav) return; const setOpen = (isOpen) => { els.menuToggle.setAttribute('aria-expanded', String(isOpen)); els.menuToggle.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu'); els.siteNav.classList.toggle('is-open', isOpen); }; els.menuToggle.addEventListener('click', () => { setOpen(els.menuToggle.getAttribute('aria-expanded') !== 'true'); }); els.siteNav.addEventListener('click', (event) => { if (event.target.closest('a')) setOpen(false); }); } function updateThreshold(selectedValue) { if (!els.minDonationLabel) return; els.minDonationLabel.textContent = selectedValue ? 'Selected charity ready. Donations still move directly to the provider; SoulWall verifies receipts and adds them to the shared impact ledger.' : 'From 1c gestures to $1,000,000 campaigns, every gated action can become a direct-to-charity contribution. The only limit to the good you can do is your imagination.'; } function normalizeProject(project) { return { provider: project.provider || state.provider || 'giveth', id: project.slug || project.id || project.nonprofitSlug || project.name, slug: project.slug || project.id || project.nonprofitSlug || '', organizationId: project.organizationId || project.organization_id || project.id || '', name: project.name || project.slug || 'Verified project', description: project.description || 'Official provider project. Donate there, then return with proof.', url: project.donateUrl || project.url || project.website || project.websiteUrl || '', donateUrl: project.donateUrl || project.url || project.website || project.websiteUrl || '', logoUrl: project.logoUrl || project.logo_url || '', network: project.network || 'Polygon', currency: project.currency || project.preferredCurrency || project.provider || 'Provider', preferredCurrency: project.preferredCurrency || project.currency || '', chainId: Number(project.chainId || project.preferredChainId || project.chainID || 0) || null, recipientAddress: project.recipientAddress || '', supporterCount: Number(project.supporterCount || project.donation_count || project.contribution_count || 0), ein: project.ein || '', category: project.category || '', website: project.website || project.websiteUrl || project.url || '', routeMode: project.provider === 'tgb' ? 'wallet' : 'provider' }; } function charityCardClass(index) { return ['charity-card-water', 'charity-card-health', 'charity-card-cash', 'charity-card-research'][index % 4]; } function truncateText(value, maxLength = 120) { const text = String(value || '').replace(/\s+/g, ' ').trim(); if (text.length <= maxLength) return text; return `${text.slice(0, maxLength - 3).trim()}...`; } function getSupporterLine(project) { const count = Number(project.supporterCount || 0); if (count > 0) return `${count.toLocaleString()} contributions recorded`; return 'Verified cause'; } function getProviderDetail(project) { if (project.provider === 'giveth') { return `${project.network || 'Polygon'} / ${project.currency || 'POL'}`; } if (project.provider === 'tgb') { return `${project.network || 'Crypto'} / ${project.currency || project.preferredCurrency || 'ETH'}`; } if (project.ein) return `EIN ${project.ein}`; return project.category || 'Direct charity rail'; } function getProjectImageStyle(project) { if (!project.logoUrl) return ''; return ` style="background-image: linear-gradient(rgba(12, 15, 17, 0.08), rgba(12, 15, 17, 0.22)), url('${escapeHtml(project.logoUrl)}');"`; } function expandHeroProjects(projects, targetCount = MAX_SOULWALL_CARDS) { if (!projects.length) return []; if (projects.length >= targetCount) { return projects.slice(0, targetCount).map((project, index) => ({ ...project, heroKey: `${project.id}-${index}` })); } const expanded = []; for (let index = 0; expanded.length < targetCount; index += 1) { const project = projects[index % projects.length]; expanded.push({ ...project, heroKey: `${project.id}-${expanded.length}` }); } return expanded; } function createProjectCard(project, index, variant = 'full') { const card = document.createElement('button'); card.type = 'button'; card.className = `charity-provider-card ${charityCardClass(index)}`; card.dataset.projectId = project.id; card.setAttribute('aria-label', `Choose ${project.name}`); if (variant === 'hero') { card.dataset.cardKey = project.heroKey || `${project.id}-${index}`; } card.innerHTML = ` ${escapeHtml(getSupporterLine(project))} ${escapeHtml(project.name)} ${escapeHtml(getProviderDetail(project))} Click To Choose `; card.addEventListener('click', () => selectProject(project, variant === 'hero' ? card.dataset.cardKey : '')); card.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectProject(project, variant === 'hero' ? card.dataset.cardKey : ''); } }); if (variant === 'hero') { card.addEventListener('mouseenter', () => { state.hoveredHeroCardKey = card.dataset.cardKey || ''; refreshHeroCardStates(); }); card.addEventListener('mouseleave', () => { state.hoveredHeroCardKey = ''; refreshHeroCardStates(); }); card.addEventListener('focus', () => { state.hoveredHeroCardKey = card.dataset.cardKey || ''; refreshHeroCardStates(); }); card.addEventListener('blur', () => { state.hoveredHeroCardKey = ''; refreshHeroCardStates(); }); } return card; } function renderHeroProjects(projects) { if (!els.heroGrid) return; if (!projects.length) { els.heroGrid.innerHTML = '

No charity projects are configured yet.

'; return; } const expandedProjects = expandHeroProjects(projects.slice(0, MAX_SOULWALL_CARDS)); els.heroGrid.innerHTML = ''; expandedProjects.forEach((project, index) => { els.heroGrid.appendChild(createProjectCard(project, index, 'hero')); }); refreshCardSelections(); refreshHeroCardStates(); } function renderProjectGrid(projects) { if (!els.grid) return; if (!projects.length) { els.grid.innerHTML = '

No charity projects are configured yet.

'; return; } els.grid.innerHTML = ''; projects.slice(0, MAX_SOULWALL_CARDS).forEach((project, index) => { els.grid.appendChild(createProjectCard(project, index, 'full')); }); refreshCardSelections(); } function renderProjects(projects) { renderHeroProjects(projects); renderProjectGrid(projects); } function refreshCardSelections() { const selectedId = state.selected?.id || ''; document.querySelectorAll('.charity-provider-card[data-project-id]').forEach((card) => { card.classList.toggle('is-selected', Boolean(selectedId) && card.dataset.projectId === selectedId); }); } function refreshHeroCardStates() { if (!els.heroGrid) return; const heroCards = Array.from(els.heroGrid.querySelectorAll('.charity-provider-card[data-card-key]')); if (!heroCards.length) return; const selectedId = state.selected?.id || ''; let featuredAssigned = false; let cascadeIndex = 0; heroCards.forEach((card, index) => { const shouldFeature = state.hoveredHeroCardKey ? card.dataset.cardKey === state.hoveredHeroCardKey : (!featuredAssigned && ( (selectedId && card.dataset.projectId === selectedId) || (!selectedId && index === 0) )); if (shouldFeature) { featuredAssigned = true; card.classList.add('is-featured'); card.classList.remove('is-cascade'); card.style.setProperty('--cascade-index', '0'); return; } cascadeIndex += 1; card.classList.remove('is-featured'); card.classList.add('is-cascade'); card.style.setProperty('--cascade-index', String(Math.min(cascadeIndex, 5))); }); } function findProjectBySlug(slug) { const normalized = String(slug || '').trim().toLowerCase(); if (!normalized) return null; return state.projects.find((project) => ( String(project.slug || '').trim().toLowerCase() === normalized || String(project.id || '').trim().toLowerCase() === normalized )) || null; } function setStepState(activeStep = 1, completedSteps = []) { const completed = new Set(completedSteps.map((value) => Number(value))); els.stepItems.forEach((item, index) => { const stepNumber = Number(item.dataset.step || index + 1); const pill = item.querySelector('.charity-step-pill'); item.classList.toggle('is-active', stepNumber === Number(activeStep)); item.classList.toggle('is-complete', completed.has(stepNumber)); if (pill) { pill.textContent = completed.has(stepNumber) ? '\u2713' : String(stepNumber); } }); } function setVerifyStatus(message, stateName = '') { if (!els.verifyStatus) return; els.verifyStatus.textContent = sanitizeSoulwallMessage(message); els.verifyStatus.classList.toggle('is-checking', stateName === 'checking'); els.verifyStatus.classList.toggle('is-success', stateName === 'success'); els.verifyStatus.classList.toggle('is-error', stateName === 'error'); } function getSelectedRoute() { return state.intent?.route || null; } function updateRouteBox() { const selected = state.selected; const route = getSelectedRoute(); const isTgb = selected?.provider === 'tgb'; if (!els.routeBox || !els.routeAddress || !els.routeLabel || !els.routeCopy) return; if (!isTgb) { els.routeBox.classList.add('is-hidden'); els.routeAddress.textContent = 'Select a TGB cause to mint an address.'; els.routeCopy.textContent = 'Copy'; els.routeCopy.disabled = true; return; } els.routeBox.classList.remove('is-hidden'); els.routeLabel.textContent = route?.network ? `Dynamic Donation Address / ${route.network}${route.pledgeCurrency ? ` / ${route.pledgeCurrency}` : ''}` : 'Dynamic Donation Address'; els.routeAddress.textContent = route?.depositAddress || 'Minting a new address for this cause.'; els.routeCopy.textContent = route?.depositAddress ? 'Copy' : 'Wait'; els.routeCopy.disabled = !route?.depositAddress; } function getStepLabels(project) { if (project?.provider === 'tgb') { return ['Pick One', 'Mint Address', 'Donate and Return', 'Verify and Unlock']; } return ['Pick One', 'Open Charity Page', 'Donate and Return', 'Verify and Unlock']; } function applyStepLabels(project) { const labels = getStepLabels(project); els.stepItems.forEach((item, index) => { const label = item.querySelector('span:last-child'); if (label && labels[index]) label.textContent = labels[index]; }); } function updateGatewayCopy() { const selected = state.selected; const route = getSelectedRoute(); const isTgb = selected?.provider === 'tgb'; applyStepLabels(selected); if (els.selectedTitle) { els.selectedTitle.textContent = selected ? selected.name : 'Open a 24-hour session'; } if (els.helperLabel) { els.helperLabel.textContent = selected ? 'Step 2 - Desktop Flow' : 'Step 1 - Pick One'; } if (els.helperCopy) { if (!selected) { els.helperCopy.textContent = 'Pick one cause above. SoulWall will show only the next action you need.'; } else if (isTgb) { els.helperCopy.textContent = route?.depositAddress ? `${selected.name} selected. Send a donation to the fresh ${route.network || 'crypto'} address below, then paste the confirmed transaction hash here to unlock.` : `${selected.name} selected. SoulWall is minting a fresh donation address for this cause.`; } else { els.helperCopy.textContent = `${selected.name} selected. Open the official charity page, donate, then paste the confirmed transaction hash here to unlock.`; } } if (els.providerLink) { if (isTgb) { els.providerLink.href = route?.depositAddress || '#'; els.providerLink.textContent = route?.depositAddress ? 'Step 2 - Copy Donation Address' : 'Step 2 - Minting Donation Address'; els.providerLink.setAttribute('aria-disabled', route?.depositAddress ? 'false' : 'true'); } else if (selected?.donateUrl || selected?.url) { els.providerLink.href = selected.donateUrl || selected.url; els.providerLink.textContent = 'Step 2 - Open Selected Charity Page'; els.providerLink.setAttribute('aria-disabled', 'false'); } else { els.providerLink.href = '#'; els.providerLink.textContent = 'Step 2 - Open Selected Charity Page'; els.providerLink.setAttribute('aria-disabled', 'true'); } } if (els.deviceFlowNote) { if (!selected) { els.deviceFlowNote.textContent = 'Step 3: choose a cause first, then SoulWall will show the next action.'; } else if (isTgb) { els.deviceFlowNote.textContent = route?.depositAddress ? `Step 3: send ${route.pledgeCurrency || selected.currency || 'crypto'} to the minted address, wait for confirmation, then paste the transaction hash below.` : 'Step 3: wait for SoulWall to mint the donation address for this cause.'; } else { els.deviceFlowNote.textContent = 'Step 3: donate on the official provider page, then paste the confirmed transaction hash below.'; } } if (els.txHashLabel) { els.txHashLabel.textContent = isTgb ? 'Already sent the donation? Paste the confirmed transaction hash from your wallet or block explorer to verify this TGB route.' : 'Already have the proof? Paste the confirmed transaction hash. You can find it in your wallet history or on the provider confirmation view.'; } updateRouteBox(); if (!selected) { setStepState(1); setVerifyStatus('Step 1: choose a cause and SoulWall will reveal the next step.'); return; } setStepState(2, [1]); setVerifyStatus( isTgb ? (route?.depositAddress ? `${selected.name} selected. Send the donation to the minted address, then return with the confirmed transaction hash.` : `${selected.name} selected. SoulWall is preparing the donation address for this route.`) : `${selected.name} selected. Open the official charity page, then return with the confirmed transaction hash.` ); } async function selectProject(project, heroCardKey = '') { state.selected = project; state.hoveredHeroCardKey = heroCardKey || state.hoveredHeroCardKey; state.intent = null; refreshCardSelections(); refreshHeroCardStates(); updateGatewayCopy(); updateThreshold(project.currency || project.provider || 'charity'); forceAdvanceToNextStep(); try { state.intent = await fetchJson('/api/soulwall/intent', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: project.provider, nonprofitSlug: project.slug || project.id, organizationId: project.organizationId || project.id || '', organizationName: project.name || '', website: project.website || project.url || '', category: project.category || '', pledgeCurrency: project.preferredCurrency || project.currency || '', clientId: deploymentMeta.clientId, deploymentTitle: deploymentMeta.deploymentTitle, deploymentDescription: deploymentMeta.deploymentDescription, sourceWebsite: deploymentMeta.sourceWebsite, integrationOrigin: deploymentMeta.integrationOrigin }) }); updateGatewayCopy(); forceAdvanceToNextStep(); } catch (error) { state.intent = null; updateGatewayCopy(); setVerifyStatus(error.message || 'SoulWall could not prepare this charity route yet.', 'error'); forceAdvanceToNextStep(); } } function formatUsd(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) return '$0.00'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: numeric >= 1000 ? 0 : 2 }).format(numeric); } function formatNativeAmount(value, currency) { const numeric = Number(value); const code = String(currency || '').trim().toUpperCase(); if (!Number.isFinite(numeric) || numeric <= 0 || !code || code === 'USD') return ''; return `${numeric.toLocaleString(undefined, { maximumFractionDigits: numeric >= 10 ? 2 : 4 })} ${code}`; } function formatLedgerAmount(row) { const usd = Number(row.amount_usd); const native = formatNativeAmount(row.amount_native, row.currency); const hasUsd = Number.isFinite(usd) && usd > 0; if (hasUsd && native) return `${formatUsd(usd)} / ${native}`; if (hasUsd) return formatUsd(usd); return native || formatUsd(0); } function formatLedgerTotal(totals) { const usd = formatUsd(totals.amountUsd); const nativeParts = Object.entries(totals.nativeByCurrency || {}) .map(([currency, amount]) => formatNativeAmount(amount, currency)) .filter(Boolean); if (!nativeParts.length) return usd; return `${usd} / ${nativeParts.join(' + ')}`; } function titleCaseSlug(value) { return String(value || '') .split(/[-_]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } function formatDeploymentLabel(row) { const explicit = String(row.deployment_title || '').trim(); if (explicit) return explicit; const clientId = String(row.client_id || '').trim().toLowerCase(); if (clientId && DEPLOYMENT_LABELS[clientId]) return DEPLOYMENT_LABELS[clientId]; if (clientId) return titleCaseSlug(clientId); return 'SoulWall'; } function formatOriginLabel(row) { const raw = String(row.source_website || row.integration_origin || '').trim(); if (!raw) return formatDeploymentLabel(row); try { const url = new URL(raw); const host = url.hostname.replace(/^www\./, ''); if (host === 'livenewsindex.com') return 'livenewsindex.com'; if (host === 'soulwall.org') return 'soulwall.org'; return host; } catch (error) { return raw.replace(/^https?:\/\//i, '').replace(/^www\./i, ''); } } function renderLedger(payload) { const totals = payload?.totals || {}; const totalLabel = formatLedgerTotal(totals); const donationCount = Number(totals.donations || 0).toLocaleString(); const projectCount = Number(totals.projects || 0).toLocaleString(); const integrationCount = Number(totals.integrations || 0).toLocaleString(); if (els.ledgerTotalUsd) els.ledgerTotalUsd.textContent = totalLabel; if (els.ledgerDonationCount) els.ledgerDonationCount.textContent = donationCount; if (els.ledgerProjectCount) els.ledgerProjectCount.textContent = projectCount; if (els.ledgerIntegrationCount) els.ledgerIntegrationCount.textContent = integrationCount; if (els.heroLedgerTotal) els.heroLedgerTotal.textContent = totalLabel; if (els.heroLedgerCaption) { els.heroLedgerCaption.textContent = payload?.configured === false ? 'Ledger storage is not configured yet.' : `${donationCount} confirmed donations recorded across the SoulWall public archive.`; } if (els.heroLedgerDonations) els.heroLedgerDonations.textContent = donationCount; if (els.heroLedgerProjects) els.heroLedgerProjects.textContent = projectCount; if (els.heroLedgerIntegrations) els.heroLedgerIntegrations.textContent = integrationCount; const recent = Array.isArray(payload?.recent) ? payload.recent : []; if (!els.ledgerList) return; if (!recent.length) { els.ledgerList.innerHTML = `

${payload?.configured === false ? 'Ledger storage is not configured yet.' : 'No confirmed donation pings have reached the public archive yet.'}

`; return; } els.ledgerList.innerHTML = recent.map((row) => { const project = row.project_name || row.project_slug || 'Verified project'; const provider = row.provider || 'provider'; const origin = formatOriginLabel(row); const deploymentTitle = formatDeploymentLabel(row); const deploymentDescription = row.deployment_description || ''; const stamp = row.confirmed_at ? new Date(row.confirmed_at).toLocaleString() : 'confirmed'; return `
${escapeHtml(project)} ${escapeHtml(provider)} / ${escapeHtml(origin)} / ${escapeHtml(stamp)} ${deploymentTitle || deploymentDescription ? `${escapeHtml( deploymentDescription ? `${deploymentTitle || 'SoulWall deployment'} - ${deploymentDescription}` : deploymentTitle )}` : ''}
${escapeHtml(formatLedgerAmount(row))}
`; }).join(''); } async function loadLedger() { if (els.ledgerList) { els.ledgerList.innerHTML = '

Syncing public archive.

'; } if (els.heroLedgerCaption) { els.heroLedgerCaption.textContent = 'Syncing the live public archive.'; } try { const payload = await fetchJson('/api/soulwall/ledger'); renderLedger(payload); } catch (error) { if (els.ledgerList) { els.ledgerList.innerHTML = `

${escapeHtml(sanitizeSoulwallMessage(error.message))}

`; } if (els.heroLedgerCaption) { els.heroLedgerCaption.textContent = sanitizeSoulwallMessage(error.message); } } } function formatDuration(seconds) { const safeSeconds = Math.max(0, Math.floor(Number(seconds) || 0)); const hours = Math.floor(safeSeconds / 3600); const minutes = Math.floor((safeSeconds % 3600) / 60); const remainingSeconds = safeSeconds % 60; return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`; } function updateSessionUi() { if (els.accessText) { els.accessText.textContent = state.session.access ? `Access open / ${formatDuration(state.session.secondsRemaining)} remaining` : 'Locked until provider confirmation'; } if (els.windowText) { els.windowText.textContent = state.session.access && state.session.expiresAt ? `Open until ${new Date(state.session.expiresAt).toLocaleString([], { weekday: 'short', day: 'numeric', month: 'short', hour: 'numeric', minute: '2-digit' })}` : '24 Hours of Access'; } } async function loadSession() { try { const session = await fetchJson('/api/soulwall/session'); state.session = { access: Boolean(session.access), expiresAt: session.expiresAt || '', secondsRemaining: Number(session.secondsRemaining || 0) }; } catch (error) { state.session = { access: false, expiresAt: '', secondsRemaining: 0 }; } updateSessionUi(); } async function loadProjects() { if (els.grid) els.grid.innerHTML = '

Syncing verified charity network.

'; try { const payload = await fetchJson('/api/soulwall/charities'); state.provider = payload.provider || 'giveth'; state.projects = (payload.charities || []).map(normalizeProject); renderProjects(state.projects); } catch (error) { console.warn('[SoulWall] Charity cards falling back to local defaults:', error); state.provider = 'giveth'; state.projects = LOCAL_FALLBACK_PROJECTS.map(normalizeProject); renderProjects(state.projects); } updateGatewayCopy(); } async function verifyProof(event) { event.preventDefault(); const txHash = els.txHash?.value.trim() || ''; const isTgb = state.selected?.provider === 'tgb'; if (!state.selected) { setVerifyStatus('Pick one cause first, then follow the Step 2 action SoulWall shows you.', 'error'); return; } if (!txHash) { setVerifyStatus('Paste the confirmed transaction hash before verifying.', 'error'); return; } setStepState(3, [1, 2]); setVerifyStatus('Checking receipt against the selected charity path.', 'checking'); try { const payload = await fetchJson(isTgb ? '/api/soulwall/tgb-verify' : '/api/soulwall/giveth-verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(isTgb ? { txHash, intentId: state.intent?.partnerDonationId || '', partnerDonationId: state.intent?.partnerDonationId || '', route: state.intent?.route || null, clientId: deploymentMeta.clientId, deploymentTitle: deploymentMeta.deploymentTitle, deploymentDescription: deploymentMeta.deploymentDescription, sourceWebsite: deploymentMeta.sourceWebsite, integrationOrigin: deploymentMeta.integrationOrigin } : { txHash, projectSlug: state.selected.slug || state.selected.id || '', partnerDonationId: state.intent?.partnerDonationId || '', clientId: deploymentMeta.clientId, deploymentTitle: deploymentMeta.deploymentTitle, deploymentDescription: deploymentMeta.deploymentDescription, sourceWebsite: deploymentMeta.sourceWebsite, integrationOrigin: deploymentMeta.integrationOrigin }) }); if (payload.unlocked || payload.proofRecognized || payload.duplicate) { const verifiedProject = findProjectBySlug(payload.projectSlug) || state.selected; if (verifiedProject?.id) { state.selected = verifiedProject; refreshCardSelections(); } setStepState(4, [1, 2, 3, 4]); setVerifyStatus(payload.message || `${verifiedProject?.name || 'Selected charity'} accepted and recorded in the public ledger.`, 'success'); await loadSession(); await loadLedger(); return; } setVerifyStatus(payload.message || 'Receipt not found yet. Give the provider a moment, then try again.', 'error'); } catch (error) { setVerifyStatus(error.message, 'error'); } } function burstConfettiFromElement(element, count = 18) { if (!element) return; const colors = ['#ec4899', '#f59e0b', '#10b981', '#3b82f6']; const origin = element.getBoundingClientRect(); const baseX = origin.left + origin.width / 2; const baseY = origin.top + origin.height / 2; for (let index = 0; index < count; index += 1) { const particle = document.createElement('span'); particle.className = 'mercy-particle'; particle.style.setProperty('--particle-color', colors[index % colors.length]); particle.style.setProperty('--x', `${baseX + (Math.random() - 0.5) * origin.width}px`); particle.style.setProperty('--y', `${baseY + (Math.random() - 0.5) * 18}px`); particle.style.setProperty('--drift', `${(Math.random() - 0.5) * 150}px`); document.body.appendChild(particle); window.setTimeout(() => particle.remove(), 1500); } } async function copyRouteAddress() { const route = getSelectedRoute(); if (!route?.depositAddress) return; try { await navigator.clipboard.writeText(route.depositAddress); if (els.routeCopy) els.routeCopy.textContent = 'Copied'; window.setTimeout(() => { if (els.routeCopy) els.routeCopy.textContent = 'Copy'; }, 1400); } catch (error) { setVerifyStatus('SoulWall could not copy the donation address automatically. Select and copy it manually.', 'error'); } } function handleProviderAction(event) { const selected = state.selected; const route = getSelectedRoute(); if (selected?.provider !== 'tgb') return; if (!route?.depositAddress) { event.preventDefault(); setVerifyStatus('SoulWall is still minting the donation address for this cause.', 'checking'); return; } event.preventDefault(); copyRouteAddress(); } if (els.refresh) els.refresh.addEventListener('click', loadProjects); if (els.proofForm) els.proofForm.addEventListener('submit', verifyProof); if (els.ledgerRefresh) els.ledgerRefresh.addEventListener('click', loadLedger); if (els.providerLink) els.providerLink.addEventListener('click', handleProviderAction); if (els.routeCopy) els.routeCopy.addEventListener('click', copyRouteAddress); if (els.impactConfetti) { els.impactConfetti.addEventListener('click', () => burstConfettiFromElement(els.impactConfetti)); } updateThreshold(''); setupMobileMenu(); updateGatewayCopy(); loadSession(); loadProjects(); loadLedger();