From e4095c77c3a5ef04e490e427553e85a9888fcc67 Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Tue, 3 Mar 2026 20:59:37 +0700 Subject: [PATCH 1/2] feat: professionalize pro checkout and migration workflow --- .github/FUNDING.yml | 2 +- README.md | 4 +- docs/commercial/checkout-migration.md | 86 +++++++++++++ docs/commercial/customer-migration-email.md | 20 +++ scripts/commerce/polar-webhook-example.ts | 115 ++++++++++++++++++ src/components/Sidebar.tsx | 4 +- src/components/onboarding/Onboarding.tsx | 4 +- .../settings/about/AboutSettings.tsx | 15 +-- src/config/commerce.ts | 2 + src/utils/commerce.ts | 10 ++ 10 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 docs/commercial/checkout-migration.md create mode 100644 docs/commercial/customer-migration-email.md create mode 100644 scripts/commerce/polar-webhook-example.ts create mode 100644 src/config/commerce.ts create mode 100644 src/utils/commerce.ts diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 22bf594..3988144 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: 0xNyk -custom: ["https://0xnyk.gumroad.com/l/dictx"] +custom: ["https://buy.splitlabs.io"] diff --git a/README.md b/README.md index dcf1e15..136b616 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

Download   - Get Dictx Pro + Get Dictx Pro

@@ -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://buy.splitlabs.io) ($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..daaf13f --- /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://buy.splitlabs.io` 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 `buy.splitlabs.io` DNS record and point it to your checkout provider. +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://buy.splitlabs.io` +- In-app CTA links point to a shared `PRO_PURCHASE_URL` +- GitHub funding link points to `https://buy.splitlabs.io` + +## 5) Migration Messaging + +1. Send announcement to existing buyers. +2. Publish FAQ with key points: + +- Existing licenses remain honored +- New purchases happen at `buy.splitlabs.io` +- 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 `buy.splitlabs.io` + +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..0278019 --- /dev/null +++ b/docs/commercial/customer-migration-email.md @@ -0,0 +1,20 @@ +Subject: Dictx Pro checkout is now at buy.splitlabs.io + +Hi, + +We upgraded Dictx Pro purchasing to a new hosted checkout at: +https://buy.splitlabs.io + +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/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 = ({ })}

diff --git a/src/components/settings/about/AboutSettings.tsx b/src/components/settings/about/AboutSettings.tsx index b8a7de3..231d153 100644 --- a/src/components/settings/about/AboutSettings.tsx +++ b/src/components/settings/about/AboutSettings.tsx @@ -5,6 +5,7 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import { SettingsGroup } from "../../ui/SettingsGroup"; import { SettingContainer } from "../../ui/SettingContainer"; import { Button } from "../../ui/Button"; +import { openProPurchasePage } from "@/utils/commerce"; import { AppDataDirectory } from "../AppDataDirectory"; import { AppLanguageSelector } from "../AppLanguageSelector"; import { LogDirectory } from "../debug"; @@ -27,14 +28,6 @@ export const AboutSettings: React.FC = () => { fetchVersion(); }, []); - const handleGetProClick = async () => { - try { - await openUrl("https://0xnyk.gumroad.com/l/dictx"); - } catch (error) { - console.error("Failed to open purchase link:", error); - } - }; - return (
@@ -53,7 +46,11 @@ export const AboutSettings: React.FC = () => { description={t("settings.about.supportDevelopment.description")} grouped={true} > - diff --git a/src/config/commerce.ts b/src/config/commerce.ts new file mode 100644 index 0000000..1585b06 --- /dev/null +++ b/src/config/commerce.ts @@ -0,0 +1,2 @@ +export const PRO_PURCHASE_URL = "https://buy.splitlabs.io"; +export const PRO_SUPPORT_EMAIL = "support@splitlabs.io"; diff --git a/src/utils/commerce.ts b/src/utils/commerce.ts new file mode 100644 index 0000000..d96cc56 --- /dev/null +++ b/src/utils/commerce.ts @@ -0,0 +1,10 @@ +import { openUrl } from "@tauri-apps/plugin-opener"; +import { PRO_PURCHASE_URL } from "@/config/commerce"; + +export const openProPurchasePage = async (): Promise => { + try { + await openUrl(PRO_PURCHASE_URL); + } catch (error) { + console.error("Failed to open purchase link:", error); + } +}; From 76f6f0acd055d621310b2997942e319363b1a82a Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Tue, 3 Mar 2026 21:04:00 +0700 Subject: [PATCH 2/2] feat: add dictx.splitlabs.io landing site and vercel routing --- .github/FUNDING.yml | 2 +- README.md | 4 +- docs/commercial/checkout-migration.md | 12 +- docs/commercial/customer-migration-email.md | 4 +- site/index.html | 152 ++++++++++ site/styles.css | 311 ++++++++++++++++++++ src/config/commerce.ts | 2 +- vercel.json | 26 ++ 8 files changed, 501 insertions(+), 12 deletions(-) create mode 100644 site/index.html create mode 100644 site/styles.css create mode 100644 vercel.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3988144..0f3756e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: 0xNyk -custom: ["https://buy.splitlabs.io"] +custom: ["https://dictx.splitlabs.io/buy"] diff --git a/README.md b/README.md index 136b616..996231f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

Download   - Get Dictx Pro + Get Dictx Pro

@@ -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://buy.splitlabs.io) ($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 index daaf13f..a88588d 100644 --- a/docs/commercial/checkout-migration.md +++ b/docs/commercial/checkout-migration.md @@ -1,6 +1,6 @@ # Dictx Commerce Migration (Gumroad -> Professional Checkout) -This runbook moves Dictx Pro sales to `https://buy.splitlabs.io` while keeping the OSS/free path unchanged. +This runbook moves Dictx Pro sales to `https://dictx.splitlabs.io/buy` while keeping the OSS/free path unchanged. ## Scope @@ -10,7 +10,7 @@ This runbook moves Dictx Pro sales to `https://buy.splitlabs.io` while keeping t ## 1) Domain + Checkout -1. Create `buy.splitlabs.io` DNS record and point it to your checkout provider. +1. Create `dictx.splitlabs.io` as your Vercel custom domain and serve the landing site there. 2. Configure branded checkout: - Product name: `Dictx Pro` @@ -53,9 +53,9 @@ Implementation reference: Completed in this repo: -- `README` Pro links now point to `https://buy.splitlabs.io` +- `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://buy.splitlabs.io` +- GitHub funding link points to `https://dictx.splitlabs.io/buy` ## 5) Migration Messaging @@ -63,7 +63,7 @@ Completed in this repo: 2. Publish FAQ with key points: - Existing licenses remain honored -- New purchases happen at `buy.splitlabs.io` +- New purchases go through `https://dictx.splitlabs.io/buy` - Support contact stays unchanged 3. Use template: @@ -78,7 +78,7 @@ Before launch: - 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 `buy.splitlabs.io` +- Purchase links from app + README resolve to `https://dictx.splitlabs.io/buy` After launch: diff --git a/docs/commercial/customer-migration-email.md b/docs/commercial/customer-migration-email.md index 0278019..fff0467 100644 --- a/docs/commercial/customer-migration-email.md +++ b/docs/commercial/customer-migration-email.md @@ -1,9 +1,9 @@ -Subject: Dictx Pro checkout is now at buy.splitlabs.io +Subject: Dictx Pro checkout is now at dictx.splitlabs.io/buy Hi, We upgraded Dictx Pro purchasing to a new hosted checkout at: -https://buy.splitlabs.io +https://dictx.splitlabs.io/buy What this means: 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 + + + + + + + +

+
+ Dictx + +
+ +
+
+

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. +

+
+
+
+ +
+

© Split Labs. Dictx by 0xNyk.

+
+ + + + 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/config/commerce.ts b/src/config/commerce.ts index 1585b06..9ae9c90 100644 --- a/src/config/commerce.ts +++ b/src/config/commerce.ts @@ -1,2 +1,2 @@ -export const PRO_PURCHASE_URL = "https://buy.splitlabs.io"; +export const PRO_PURCHASE_URL = "https://dictx.splitlabs.io/buy"; export const PRO_SUPPORT_EMAIL = "support@splitlabs.io"; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..570dd66 --- /dev/null +++ b/vercel.json @@ -0,0 +1,26 @@ +{ + "redirects": [ + { + "source": "/buy", + "destination": "https://buy.splitlabs.io", + "statusCode": 307 + } + ], + "rewrites": [ + { + "source": "/", + "destination": "/site/index.html" + } + ], + "headers": [ + { + "source": "/site/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=600" + } + ] + } + ] +}