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 @@
-
+
@@ -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 = ({
})}