diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 22bf594..0f3756e 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,2 @@
github: 0xNyk
-custom: ["https://0xnyk.gumroad.com/l/dictx"]
+custom: ["https://dictx.splitlabs.io/buy"]
diff --git a/README.md b/README.md
index dcf1e15..996231f 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -28,7 +28,7 @@
---
-> **Free and open source.** Dictx is GPL-3.0 licensed — you can build it from source with full functionality. [Buy Dictx Pro](https://0xnyk.gumroad.com/l/dictx) ($29 one-time) for a signed binary, auto-updates, and to support development.
+> **Free and open source.** Dictx is GPL-3.0 licensed — you can build it from source with full functionality. [Buy Dictx Pro](https://dictx.splitlabs.io/buy) ($29 one-time) for a signed binary, auto-updates, and to support development.
Dictx is a cross-platform desktop application for speech transcription. Press a shortcut, speak, and your words appear in any text field — no cloud, no API keys, no data leaving your computer.
diff --git a/docs/commercial/checkout-migration.md b/docs/commercial/checkout-migration.md
new file mode 100644
index 0000000..a88588d
--- /dev/null
+++ b/docs/commercial/checkout-migration.md
@@ -0,0 +1,86 @@
+# Dictx Commerce Migration (Gumroad -> Professional Checkout)
+
+This runbook moves Dictx Pro sales to `https://dictx.splitlabs.io/buy` while keeping the OSS/free path unchanged.
+
+## Scope
+
+- Product: Dictx Pro (signed binaries + auto-updates)
+- Price: `$29` one-time
+- Free version: unchanged (GPL source build)
+
+## 1) Domain + Checkout
+
+1. Create `dictx.splitlabs.io` as your Vercel custom domain and serve the landing site there.
+2. Configure branded checkout:
+
+- Product name: `Dictx Pro`
+- Offer copy: `Signed binaries, auto-updates, and direct support`
+- Price: `USD 29 one-time`
+
+3. Enable customer portal for:
+
+- Receipts/invoices
+- Download access
+- Billing/profile management
+
+## 2) License + Entitlements
+
+Define one entitlement key:
+
+- `dictx_pro`
+
+Entitlement grants:
+
+- Access to release downloads
+- Auto-update eligibility
+- Priority support queue (if enabled)
+
+## 3) Webhook Processing
+
+Use a webhook endpoint to sync purchases to your entitlement store.
+
+Events to handle:
+
+- `order.paid` (grant entitlement)
+- `subscription.active` (if you add annual support plans)
+- `order.refunded` or `subscription.canceled` (revoke entitlement)
+
+Implementation reference:
+
+- [scripts/commerce/polar-webhook-example.ts](/Users/nyk/repos/dictx/scripts/commerce/polar-webhook-example.ts)
+
+## 4) App + Repo Link Updates
+
+Completed in this repo:
+
+- `README` Pro links now point to `https://dictx.splitlabs.io/buy`
+- In-app CTA links point to a shared `PRO_PURCHASE_URL`
+- GitHub funding link points to `https://dictx.splitlabs.io/buy`
+
+## 5) Migration Messaging
+
+1. Send announcement to existing buyers.
+2. Publish FAQ with key points:
+
+- Existing licenses remain honored
+- New purchases go through `https://dictx.splitlabs.io/buy`
+- Support contact stays unchanged
+
+3. Use template:
+
+- [customer-migration-email.md](/Users/nyk/repos/dictx/docs/commercial/customer-migration-email.md)
+
+## 6) Validation Checklist
+
+Before launch:
+
+- Checkout success flow creates receipt + customer record
+- Webhook signature validation works in production
+- `dictx_pro` entitlement is granted/revoked correctly
+- Customer portal access works from receipt email
+- Purchase links from app + README resolve to `https://dictx.splitlabs.io/buy`
+
+After launch:
+
+- Track conversion rate from in-app CTA
+- Track support tickets tagged `billing` and `license`
diff --git a/docs/commercial/customer-migration-email.md b/docs/commercial/customer-migration-email.md
new file mode 100644
index 0000000..fff0467
--- /dev/null
+++ b/docs/commercial/customer-migration-email.md
@@ -0,0 +1,20 @@
+Subject: Dictx Pro checkout is now at dictx.splitlabs.io/buy
+
+Hi,
+
+We upgraded Dictx Pro purchasing to a new hosted checkout at:
+https://dictx.splitlabs.io/buy
+
+What this means:
+
+- New purchases now happen on our new checkout page
+- Existing customers keep access to Dictx Pro
+- Dictx remains open source (GPL) and free to build from source
+
+If you need help with receipts, downloads, or license access, reply to this email and include:
+
+- Purchase email
+- Approximate purchase date
+- Platform (macOS / Windows / Linux)
+
+Thanks for supporting Dictx.
diff --git a/scripts/commerce/polar-webhook-example.ts b/scripts/commerce/polar-webhook-example.ts
new file mode 100644
index 0000000..8d5c734
--- /dev/null
+++ b/scripts/commerce/polar-webhook-example.ts
@@ -0,0 +1,115 @@
+import { createHmac, timingSafeEqual } from "node:crypto";
+
+type PolarEventData = {
+ id?: string | number;
+ product_id?: string | number;
+ customer_email?: string;
+ metadata?: Record;
+};
+
+type PolarWebhookEvent = {
+ type: string;
+ data: PolarEventData;
+};
+
+type EntitlementChange = {
+ customerEmail: string;
+ entitlement: "dictx_pro";
+ active: boolean;
+ sourceEvent: string;
+};
+
+const WEBHOOK_SECRET = process.env.POLAR_WEBHOOK_SECRET ?? "";
+const SIGNATURE_HEADER =
+ process.env.POLAR_SIGNATURE_HEADER ?? "polar-signature";
+const PORT = Number.parseInt(process.env.PORT ?? "8787", 10);
+
+const verifySignature = (payload: string, signatureHeader: string): boolean => {
+ if (!WEBHOOK_SECRET || !signatureHeader) return false;
+
+ const computed = createHmac("sha256", WEBHOOK_SECRET)
+ .update(payload, "utf8")
+ .digest("hex");
+
+ const provided = signatureHeader.trim();
+ if (computed.length !== provided.length) return false;
+
+ return timingSafeEqual(Buffer.from(computed), Buffer.from(provided));
+};
+
+const toEntitlementChange = (
+ event: PolarWebhookEvent,
+): EntitlementChange | null => {
+ const customerEmail =
+ event.data.customer_email ?? event.data.metadata?.customer_email ?? "";
+ if (!customerEmail) return null;
+
+ if (event.type === "order.paid") {
+ return {
+ customerEmail,
+ entitlement: "dictx_pro",
+ active: true,
+ sourceEvent: event.type,
+ };
+ }
+
+ if (
+ event.type === "order.refunded" ||
+ event.type === "subscription.canceled"
+ ) {
+ return {
+ customerEmail,
+ entitlement: "dictx_pro",
+ active: false,
+ sourceEvent: event.type,
+ };
+ }
+
+ return null;
+};
+
+const persistEntitlementChange = async (
+ change: EntitlementChange,
+): Promise => {
+ // TODO: Replace with DB write (e.g. Postgres/Supabase/SQLite)
+ console.log(
+ JSON.stringify({
+ action: "entitlement_sync",
+ ...change,
+ at: new Date().toISOString(),
+ }),
+ );
+};
+
+Bun.serve({
+ port: PORT,
+ routes: {
+ "/webhooks/polar": async (req: Request) => {
+ const rawBody = await req.text();
+ const signature = req.headers.get(SIGNATURE_HEADER) ?? "";
+
+ if (!verifySignature(rawBody, signature)) {
+ return new Response("invalid signature", { status: 401 });
+ }
+
+ let event: PolarWebhookEvent;
+ try {
+ event = JSON.parse(rawBody) as PolarWebhookEvent;
+ } catch (_error) {
+ return new Response("invalid json", { status: 400 });
+ }
+
+ const change = toEntitlementChange(event);
+ if (change) {
+ await persistEntitlementChange(change);
+ }
+
+ return new Response("ok", { status: 200 });
+ },
+ },
+ fetch() {
+ return new Response("not found", { status: 404 });
+ },
+});
+
+console.log(`Polar webhook example listening on http://localhost:${PORT}`);
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..72552b6
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,152 @@
+
+
+
+
+
+ Dictx | Private speech-to-text for serious writing
+
+
+
+
+
+
+
+
+
+
+
+
+ Split Labs
+
+ Talk. Edit. Ship.
+ Without sending your voice to the cloud.
+
+
+ Dictx is desktop speech-to-text for people who write all day. Local
+ transcription, polished text output, and pro-grade distribution.
+
+
+
+ $29 one-time • signed binary • auto-updates • macOS / Windows / Linux
+
+
+
+
+
+ Private by default
+
+ All core transcription runs locally on your machine. No voice data
+ routed through Dictx servers.
+
+
+
+ Fast enough for flow state
+
+ Built for real writing sessions, with quick trigger-to-text loops
+ and model options for speed or precision.
+
+
+
+ Built for Obsidian workflows
+
+ Capture, clean, and export voice notes into your vault with
+ metadata-rich output.
+
+
+
+
+
+ Choose your path
+
+
+ Free
+ $0
+
+ - Full GPL source code
+ - Build from source
+ - All core transcription features
+ - Community support
+
+ View Source
+
+
+ Pro
+ $29 one-time
+
+ - Signed installers
+ - Built-in auto-updates
+ - Professional distribution
+ - Funds active development
+
+ Get Dictx Pro
+
+
+
+
+
+ FAQ
+
+ Is Dictx still open source?
+ Yes. Dictx remains GPL-licensed and free to build from source.
+
+
+ What does Pro include?
+
+ Pro is the convenience layer: signed binaries and updater flow so
+ you do not have to compile manually.
+
+
+
+ How do updates work?
+
+ The app checks signed release metadata and applies updates in-app
+ when available.
+
+
+
+
+
+
+
+
+
+
diff --git a/site/styles.css b/site/styles.css
new file mode 100644
index 0000000..2ac35d5
--- /dev/null
+++ b/site/styles.css
@@ -0,0 +1,311 @@
+:root {
+ --bg: #f4efe6;
+ --surface: #fdf9f1;
+ --text: #191611;
+ --muted: #5d5347;
+ --line: #d5cab9;
+ --brand: #0f7d5b;
+ --brand-deep: #0a4f3a;
+ --accent: #d96a2d;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ color: var(--text);
+ background:
+ radial-gradient(circle at 15% 10%, #fff5da 0%, transparent 28%),
+ radial-gradient(circle at 88% 15%, #dceee8 0%, transparent 35%), var(--bg);
+ font-family: "Bricolage Grotesque", sans-serif;
+ line-height: 1.4;
+}
+
+.grain {
+ pointer-events: none;
+ position: fixed;
+ inset: 0;
+ opacity: 0.08;
+ background-image: radial-gradient(#000 0.3px, transparent 0.3px);
+ background-size: 3px 3px;
+}
+
+.topbar {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 20px 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.brand {
+ color: var(--brand-deep);
+ text-decoration: none;
+ font-family: "Fraunces", serif;
+ font-size: 28px;
+}
+
+.topbar nav {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+}
+
+.topbar nav a {
+ color: var(--text);
+ text-decoration: none;
+ font-size: 14px;
+}
+
+.cta-mini {
+ padding: 8px 12px;
+ border-radius: 999px;
+ background: var(--text);
+ color: #fff !important;
+}
+
+main {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 18px 24px 80px;
+}
+
+.hero {
+ background: linear-gradient(145deg, #fefbf4 0%, #f7efdf 55%, #f4e6d2 100%);
+ border: 1px solid var(--line);
+ border-radius: 28px;
+ padding: 44px;
+ box-shadow: 0 20px 45px rgba(48, 34, 10, 0.08);
+ animation: rise 500ms ease-out both;
+}
+
+.eyebrow {
+ margin: 0;
+ color: var(--brand);
+ text-transform: uppercase;
+ letter-spacing: 0.09em;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+h1 {
+ margin: 10px 0 0;
+ font-size: clamp(36px, 6vw, 68px);
+ line-height: 0.98;
+ letter-spacing: -0.025em;
+}
+
+h1 span {
+ display: block;
+ font-family: "Fraunces", serif;
+ color: var(--brand-deep);
+ font-size: 0.7em;
+ margin-top: 12px;
+}
+
+.lead {
+ max-width: 720px;
+ margin: 22px 0 0;
+ font-size: clamp(18px, 2.4vw, 24px);
+ color: var(--muted);
+}
+
+.hero-actions {
+ margin-top: 30px;
+ display: flex;
+ gap: 14px;
+ flex-wrap: wrap;
+}
+
+.btn {
+ border: 1px solid var(--text);
+ border-radius: 12px;
+ padding: 13px 18px;
+ text-decoration: none;
+ font-weight: 700;
+ transition:
+ transform 120ms ease,
+ box-shadow 120ms ease;
+}
+
+.btn:hover {
+ transform: translateY(-2px);
+}
+
+.btn-primary {
+ background: var(--brand);
+ color: #fff;
+ border-color: var(--brand-deep);
+ box-shadow: 0 10px 20px rgba(15, 125, 91, 0.25);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--text);
+}
+
+.subline {
+ margin-top: 14px;
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.pillars {
+ margin-top: 26px;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.pillars article,
+.pricing,
+.faq {
+ border: 1px solid var(--line);
+ border-radius: 18px;
+ background: var(--surface);
+}
+
+.pillars article {
+ padding: 18px;
+}
+
+.pillars h2 {
+ margin: 0;
+ font-size: 20px;
+}
+
+.pillars p {
+ margin: 10px 0 0;
+ color: var(--muted);
+}
+
+.pricing {
+ margin-top: 22px;
+ padding: 24px;
+}
+
+.pricing > h2,
+.faq h2 {
+ margin: 0;
+ font-family: "Fraunces", serif;
+ font-size: 34px;
+}
+
+.cards {
+ margin-top: 16px;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.card {
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ padding: 18px;
+ background: #fff;
+}
+
+.card.pro {
+ background: linear-gradient(170deg, #f1fff7 0%, #ebf5ff 100%);
+}
+
+.card-label {
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ font-size: 11px;
+ color: var(--muted);
+}
+
+.price {
+ margin: 10px 0;
+ font-size: 34px;
+ font-weight: 800;
+}
+
+.price span {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--muted);
+}
+
+.card ul {
+ margin: 0 0 18px;
+ padding: 0;
+ list-style: none;
+ display: grid;
+ gap: 8px;
+}
+
+.card li {
+ padding-left: 20px;
+ position: relative;
+}
+
+.card li::before {
+ content: "◆";
+ color: var(--accent);
+ position: absolute;
+ left: 0;
+ top: 0;
+ font-size: 11px;
+}
+
+.faq {
+ margin-top: 22px;
+ padding: 24px;
+}
+
+details {
+ border-top: 1px solid var(--line);
+ padding: 14px 0;
+}
+
+summary {
+ cursor: pointer;
+ font-weight: 700;
+}
+
+details p {
+ margin: 10px 0 0;
+ color: var(--muted);
+}
+
+footer {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 0 24px 36px;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+@keyframes rise {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 840px) {
+ .hero {
+ padding: 28px;
+ }
+
+ .pillars,
+ .cards {
+ grid-template-columns: 1fr;
+ }
+
+ .topbar {
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-start;
+ }
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 527cfd0..32e2738 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -1,10 +1,10 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Cog, FlaskConical, History, Info, Sparkles, Cpu } from "lucide-react";
-import { openUrl } from "@tauri-apps/plugin-opener";
import DictxTextLogo from "./icons/DictxTextLogo";
import DictxIcon from "./icons/DictxIcon";
import { useSettings } from "../hooks/useSettings";
+import { openProPurchasePage } from "@/utils/commerce";
import {
GeneralSettings,
AdvancedSettings,
@@ -131,7 +131,7 @@ export const Sidebar: React.FC = ({
})}