Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ import {
type UserVaultPosition,
} from "./defindex.ts";

type PlausibleOptions = {
props?: Record<string, string | number | boolean>;
interactive?: boolean;
};

type PlausibleFn = ((eventName: string, options?: PlausibleOptions) => void) & { q?: unknown[] };

declare global {
interface Window {
plausible?: PlausibleFn;
doNotTrack?: string;
}

interface Navigator {
msDoNotTrack?: string;
}
}

// ── Wallet kit ────────────────────────────────────────────────────────────────

StellarWalletsKit.init({
Expand Down Expand Up @@ -335,6 +353,47 @@ const fmt = (n: number, d = 2) =>
n.toLocaleString("en-US", { maximumFractionDigits: d, minimumFractionDigits: d });
const aprToApy = (apr: number) => (Math.exp(apr / 100) - 1) * 100;
const fmtAddr = (addr: string) => addr.slice(0, 6) + "…" + addr.slice(-4);
const ANALYTICS_DOMAIN = "turbolong.com";

function doNotTrackEnabled(): boolean {
return navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
}

function ensurePlausibleQueue() {
if (window.plausible) return;
const queued = ((eventName: string, options?: PlausibleOptions) => {
queued.q = queued.q || [];
queued.q.push([eventName, options]);
}) as PlausibleFn;
window.plausible = queued;
}

function initPrivacyAnalytics() {
if (doNotTrackEnabled()) return;
ensurePlausibleQueue();
if (!document.querySelector('script[data-turbolong-analytics="plausible"]')) {
const script = document.createElement("script");
script.defer = true;
script.src = "https://plausible.io/js/script.manual.js";
script.dataset.domain = ANALYTICS_DOMAIN;
script.dataset.turbolongAnalytics = "plausible";
document.head.appendChild(script);
}
trackFunnelEvent("App Loaded", { surface: "app" }, false);
}

function trackFunnelEvent(eventName: string, props: Record<string, string | number | boolean> = {}, interactive = true) {
if (doNotTrackEnabled()) return;
ensurePlausibleQueue();
window.plausible?.(eventName, {
interactive,
props: {
surface: "app",
network: getActiveNetwork(),
...props,
},
});
}

// ── Skeleton loading (#3) ────────────────────────────────────────────────────

Expand Down Expand Up @@ -1378,6 +1437,12 @@ async function openPosition() {
await signAndSubmit(submitXdr, `Open ${liveAsset.symbol} leverage`, 1);
hideTxStepper();
savePnlEntry(liveAsset.id, selectedPool.id, initial);
trackFunnelEvent("Deposit Submitted", {
flow: "leverage",
pool: selectedPool.name,
asset: liveAsset.symbol,
leverage,
});
await loadAll();
} catch (e: any) {
markStepperError(2);
Expand Down Expand Up @@ -1585,6 +1650,12 @@ async function addFundsToPosition() {
const newDeposit = (existingPnl?.deposit ?? 0) + additional;
savePnlEntry(liveAsset.id, selectedPool.id, newDeposit);
($("add-funds-input") as HTMLInputElement).value = "";
trackFunnelEvent("Deposit Submitted", {
flow: "add_funds",
pool: selectedPool.name,
asset: liveAsset.symbol,
leverage,
});
await loadAll();
} catch (e: any) {
markStepperError(2);
Expand Down Expand Up @@ -1721,6 +1792,7 @@ async function connect() {
buildAssetTabs();
renderPoolFooter();
await loadAll();
trackFunnelEvent("Connect Wallet", { flow: "wallet" });
} catch (e: any) {
if (e?.message !== "User closed the modal") toast("Failed to connect wallet", "error");
}
Expand Down Expand Up @@ -2260,6 +2332,7 @@ updatePreview();
renderTxHistory();
renderPoolFooter();
initTooltips();
initPrivacyAnalytics();

// ── Overview (cross-protocol dashboard) ───────────────────────────────────────

Expand Down Expand Up @@ -2649,6 +2722,11 @@ $("vault-deposit-btn").addEventListener("click", async () => {
const xdr = await buildVaultDepositXdr(vault, userAddress, amount);
const { signedTxXdr } = await StellarWalletsKit.signTransaction(xdr);
await submitSignedXdr(signedTxXdr);
trackFunnelEvent("Deposit Submitted", {
flow: "vault",
pool: "DeFindex",
asset: vault.assetSymbol,
});
await refreshVaultView();
($("vault-deposit-input") as HTMLInputElement).value = "";
} catch (err: any) {
Expand Down
43 changes: 37 additions & 6 deletions landing/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<a href="#how" class="nav-link">How It Works</a>
<a href="#features" class="nav-link">Features</a>
</div>
<a href="https://app.turbolong.com" class="btn btn-primary">Launch App</a>
<a href="https://app.turbolong.com" class="btn btn-primary" data-analytics-event="Launch App" data-analytics-target="nav">Launch App</a>
</nav>

<!-- Hero -->
Expand All @@ -42,8 +42,8 @@
<h1>Leverage trading on Stellar. Simplified.</h1>
<p class="hero-sub">Open leveraged long positions on Blend Protocol pools with a single click. No perps, no funding rates &mdash; just recursive lending loops.</p>
<div class="hero-cta">
<a href="https://app.turbolong.com" class="btn btn-primary btn-lg">Launch App</a>
<a href="https://app.turbolong.com#demo" class="btn btn-ghost btn-lg">View Demo</a>
<a href="https://app.turbolong.com" class="btn btn-primary btn-lg" data-analytics-event="Launch App" data-analytics-target="hero">Launch App</a>
<a href="https://app.turbolong.com#demo" class="btn btn-ghost btn-lg" data-analytics-event="View Demo" data-analytics-target="hero">View Demo</a>
</div>
</section>

Expand Down Expand Up @@ -137,7 +137,7 @@ <h3>Built on Stellar</h3>
<section class="cta-section">
<h2>Start leveraging on Stellar</h2>
<p>Connect your Stellar wallet and open your first position in minutes.</p>
<a href="https://app.turbolong.com" class="btn btn-primary btn-lg">Launch App</a>
<a href="https://app.turbolong.com" class="btn btn-primary btn-lg" data-analytics-event="Launch App" data-analytics-target="cta">Launch App</a>
</section>

<!-- Footer -->
Expand All @@ -146,13 +146,44 @@ <h2>Start leveraging on Stellar</h2>
<a href="https://docs.blend.capital/" target="_blank" rel="noopener">Blend Docs</a>
<a href="https://github.com/blend-capital" target="_blank" rel="noopener">GitHub</a>
<a href="https://stellar.expert" target="_blank" rel="noopener">Stellar Expert</a>
<a href="privacy.html">Privacy</a>
</div>
<p class="footer-disclaimer">Turbolong is an experimental DeFi tool. Leveraged positions carry significant financial risk, including the complete loss of deposited funds. Use at your own risk. Not available in jurisdictions where cryptocurrency is prohibited.</p>
<p class="footer-copy">&copy; 2026 Turbolong</p>
</footer>

</div>
<!-- Cloudflare Web Analytics (free) — replace token after enabling in CF dashboard -->
<!-- <script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "YOUR_CF_ANALYTICS_TOKEN"}'></script> -->
<script>
(function () {
var dnt = navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
if (dnt) return;

window.plausible = window.plausible || function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};

var script = document.createElement("script");
script.defer = true;
script.src = "https://plausible.io/js/script.manual.js";
script.dataset.domain = "turbolong.com";
document.head.appendChild(script);

function track(eventName, props, interactive) {
window.plausible(eventName, {
interactive: interactive !== false,
props: Object.assign({ surface: "landing" }, props || {})
});
}

track("Landing Viewed", {}, false);
document.querySelectorAll("[data-analytics-event]").forEach(function (el) {
el.addEventListener("click", function () {
track(el.getAttribute("data-analytics-event") || "Landing Click", {
target: el.getAttribute("data-analytics-target") || "unknown"
});
});
});
})();
</script>
</body>
</html>
46 changes: 46 additions & 0 deletions landing/privacy.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Turbolong Privacy</title>
<meta name="description" content="Turbolong privacy notice for cookie-free analytics and wallet data handling." />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="landing">
<nav class="nav">
<div class="nav-brand">
<span class="nav-name">Turbo<span class="accent">long</span></span>
</div>
<a href="index.html" class="btn btn-ghost">Back</a>
</nav>

<section class="features-section">
<h1>Privacy</h1>
<div class="features">
<div class="feature">
<h3>Cookie-free analytics</h3>
<p>Turbolong uses Plausible-style custom events for aggregate funnel metrics only. The integration does not set tracking cookies and does not create cross-site advertising profiles.</p>
</div>
<div class="feature">
<h3>No wallet PII</h3>
<p>Analytics events never include wallet addresses, email addresses, transaction hashes, exact deposit amounts, IP-derived identities, or free-form user input.</p>
</div>
<div class="feature">
<h3>Tracked funnel events</h3>
<p>The landing page tracks page view and launch/demo clicks. The app tracks app load, successful wallet connection, and submitted deposit flows using coarse properties such as network, pool name, asset symbol, leverage, and flow type.</p>
</div>
<div class="feature">
<h3>Do Not Track</h3>
<p>If your browser sends Do Not Track, Turbolong does not load the analytics script and does not queue analytics events.</p>
</div>
</div>
</section>
</main>
</body>
</html>