From 9192ee2dd2c3ed96c9db04c3d10864ad4d777755 Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Wed, 4 Mar 2026 07:55:26 +0700 Subject: [PATCH] fix: switch pro activation flow to license key only --- docs/commercial/checkout-migration.md | 4 +- landing/README.md | 2 +- landing/api/pro/verify.js | 21 ++----- landing/index.html | 5 +- src-tauri/src/commands/pro.rs | 44 ++++++------- src-tauri/src/settings.rs | 4 ++ .../settings/about/AboutSettings.tsx | 63 ++++--------------- src/i18n/locales/en/translation.json | 8 +-- src/stores/proEntitlementStore.ts | 8 +-- src/utils/proEntitlement.ts | 7 +-- 10 files changed, 58 insertions(+), 108 deletions(-) diff --git a/docs/commercial/checkout-migration.md b/docs/commercial/checkout-migration.md index e82864c..ebe3c3c 100644 --- a/docs/commercial/checkout-migration.md +++ b/docs/commercial/checkout-migration.md @@ -45,7 +45,7 @@ Entitlement grants: Activation flow in app: - User opens **Settings -> About -> Upgrade to Dictx Pro** -- User enters purchase email + Polar checkout ID (`polar_cl_...`) +- User enters license key (`polar_cl_...`) - App verifies against `https://dictx.splitlabs.io/api/pro/verify` - On success, app stores active entitlement and enables updater checks @@ -91,7 +91,7 @@ Before launch: - Checkout success flow creates receipt + customer record - Webhook signature validation works in production - `dictx_pro` entitlement is granted/revoked correctly -- `landing/api/pro/verify` returns `{ active: true }` only for valid paid checkout + matching email +- `landing/api/pro/verify` returns `{ active: true }` only for valid paid checkout key - Customer portal access works from receipt email - Purchase links from app + README resolve to `https://dictx.splitlabs.io/buy` diff --git a/landing/README.md b/landing/README.md index c7752d3..25f264d 100644 --- a/landing/README.md +++ b/landing/README.md @@ -17,7 +17,7 @@ Attach `dictx.splitlabs.io` to this Vercel project. - `/` serves `landing/index.html` - `/buy` redirects to Polar checkout -- `/api/pro/verify` validates a Polar checkout/email pair for in-app Pro activation +- `/api/pro/verify` validates a Polar license key (`polar_cl_...`) for in-app Pro activation ## Environment Variables (Vercel) diff --git a/landing/api/pro/verify.js b/landing/api/pro/verify.js index 5e684be..1469f2e 100644 --- a/landing/api/pro/verify.js +++ b/landing/api/pro/verify.js @@ -5,8 +5,6 @@ const ALLOWED_PRODUCT_IDS = (process.env.POLAR_DICTX_PRODUCT_IDS || "") .map((value) => value.trim()) .filter(Boolean); -const normalizeEmail = (value) => value.trim().toLowerCase(); - const readBody = (req) => { if (!req.body) return {}; if (typeof req.body === "string") { @@ -42,17 +40,16 @@ module.exports = async (req, res) => { } const body = readBody(req); - const checkoutId = (body.checkoutId || "").trim(); - const email = normalizeEmail(body.email || ""); + const licenseKey = (body.licenseKey || body.checkoutId || "").trim(); - if (!checkoutId || !email) { - res.status(400).json({ error: "checkoutId_and_email_required" }); + if (!licenseKey) { + res.status(400).json({ error: "licenseKey_required" }); return; } try { const response = await fetch( - `${POLAR_API_BASE}/checkouts/${encodeURIComponent(checkoutId)}`, + `${POLAR_API_BASE}/checkouts/${encodeURIComponent(licenseKey)}`, { method: "GET", headers: { @@ -74,21 +71,13 @@ module.exports = async (req, res) => { } const checkout = await response.json(); - const checkoutEmail = normalizeEmail( - checkout.customer_email || - checkout.customer?.email || - checkout.metadata?.customer_email || - "", - ); - const productId = checkout.product_id == null ? "" : String(checkout.product_id); const productAllowed = ALLOWED_PRODUCT_IDS.length === 0 || ALLOWED_PRODUCT_IDS.includes(productId); - const emailMatches = checkoutEmail !== "" && checkoutEmail === email; const paid = statusLooksPaid(checkout.status, checkout.paid); - res.status(200).json({ active: Boolean(productAllowed && emailMatches && paid) }); + res.status(200).json({ active: Boolean(productAllowed && paid) }); } catch (error) { res.status(500).json({ error: "internal_error", diff --git a/landing/index.html b/landing/index.html index 526fb91..387bf3c 100644 --- a/landing/index.html +++ b/landing/index.html @@ -201,8 +201,9 @@

FAQ

How do I activate Pro inside the app?

- Open Settings, go to About, and enter your purchase email and - Polar checkout ID (starts with polar_cl_). + Open Settings, go to About, and enter your Pro license key + (currently your Polar checkout key, starts with + polar_cl_).

diff --git a/src-tauri/src/commands/pro.rs b/src-tauri/src/commands/pro.rs index c6dad03..b5b5475 100644 --- a/src-tauri/src/commands/pro.rs +++ b/src-tauri/src/commands/pro.rs @@ -9,8 +9,7 @@ const DEFAULT_PRO_VERIFY_URL: &str = "https://dictx.splitlabs.io/api/pro/verify" #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct VerifyRequest { - checkout_id: String, - email: String, + license_key: String, } #[derive(Debug, Deserialize)] @@ -23,18 +22,13 @@ fn get_verify_url() -> String { std::env::var("DICTX_PRO_VERIFY_URL").unwrap_or_else(|_| DEFAULT_PRO_VERIFY_URL.to_string()) } -fn normalize_email(email: &str) -> String { - email.trim().to_lowercase() -} - -async fn verify_checkout(checkout_id: &str, email: &str) -> Result { +async fn verify_checkout(license_key: &str) -> Result { let payload = VerifyRequest { - checkout_id: checkout_id.trim().to_string(), - email: normalize_email(email), + license_key: license_key.trim().to_string(), }; - if payload.checkout_id.is_empty() || payload.email.is_empty() { - return Err("Checkout ID and email are required".to_string()); + if payload.license_key.is_empty() { + return Err("License key is required".to_string()); } let client = reqwest::Client::new(); @@ -87,20 +81,20 @@ pub fn clear_pro_entitlement(app: AppHandle) -> Result { #[specta::specta] pub async fn activate_pro_entitlement( app: AppHandle, - checkout_id: String, - email: String, + license_key: String, ) -> Result { - let active = verify_checkout(&checkout_id, &email).await?; + let active = verify_checkout(&license_key).await?; if !active { - return Err("No active Dictx Pro entitlement found for this checkout/email".to_string()); + return Err("No active Dictx Pro entitlement found for this license key".to_string()); } let now = Utc::now().timestamp(); let mut app_settings = settings::get_settings(&app); app_settings.pro_entitlement = ProEntitlement { active: true, - email: Some(normalize_email(&email)), - checkout_id: Some(checkout_id.trim().to_string()), + license_key: Some(license_key.trim().to_string()), + email: None, + checkout_id: None, activated_at: Some(now), last_verified_at: Some(now), verification_error: None, @@ -117,20 +111,22 @@ pub async fn activate_pro_entitlement( pub async fn refresh_pro_entitlement(app: AppHandle) -> Result { let mut app_settings = settings::get_settings(&app); - let checkout_id = match app_settings.pro_entitlement.checkout_id.clone() { - Some(value) if !value.trim().is_empty() => value, - _ => return Ok(app_settings.pro_entitlement), - }; - - let email = match app_settings.pro_entitlement.email.clone() { + // Support legacy activations by falling back to checkout_id if license_key is missing. + let license_key = match app_settings + .pro_entitlement + .license_key + .clone() + .or_else(|| app_settings.pro_entitlement.checkout_id.clone()) + { Some(value) if !value.trim().is_empty() => value, _ => return Ok(app_settings.pro_entitlement), }; let now = Utc::now().timestamp(); - match verify_checkout(&checkout_id, &email).await { + match verify_checkout(&license_key).await { Ok(true) => { app_settings.pro_entitlement.active = true; + app_settings.pro_entitlement.license_key = Some(license_key); app_settings.pro_entitlement.last_verified_at = Some(now); app_settings.pro_entitlement.verification_error = None; } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 3265f1b..e219cfa 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -280,8 +280,12 @@ pub struct ProEntitlement { #[serde(default)] pub active: bool, #[serde(default)] + pub license_key: Option, + #[serde(default)] + // Legacy field kept for backward compatibility with older local settings. pub email: Option, #[serde(default)] + // Legacy field kept for backward compatibility with older local settings. pub checkout_id: Option, #[serde(default)] pub activated_at: Option, diff --git a/src/components/settings/about/AboutSettings.tsx b/src/components/settings/about/AboutSettings.tsx index 3e0e3d6..4d11db3 100644 --- a/src/components/settings/about/AboutSettings.tsx +++ b/src/components/settings/about/AboutSettings.tsx @@ -15,16 +15,12 @@ import { useProEntitlement } from "@/hooks/useProEntitlement"; export const AboutSettings: React.FC = () => { const { t } = useTranslation(); const [version, setVersion] = useState(""); - const [checkoutId, setCheckoutId] = useState(""); - const [email, setEmail] = useState(""); + const [licenseKey, setLicenseKey] = useState(""); const { entitlement, - isLoading: proLoading, isSubmitting: proSubmitting, error: proError, activate, - refresh, - clear, } = useProEntitlement(); useEffect(() => { @@ -42,9 +38,9 @@ export const AboutSettings: React.FC = () => { }, []); const handleActivate = async () => { - const ok = await activate(checkoutId, email); + const ok = await activate(licenseKey); if (ok) { - setCheckoutId(""); + setLicenseKey(""); } }; @@ -76,24 +72,6 @@ export const AboutSettings: React.FC = () => { > {t("settings.about.supportDevelopment.button")} - - {entitlement?.active && ( - - )}
@@ -102,38 +80,23 @@ export const AboutSettings: React.FC = () => { ? t("settings.about.proActivation.active") : t("settings.about.proActivation.inactive")}

- {entitlement?.email && ( -

- {t("settings.about.proActivation.email")}: {entitlement.email} -

- )} - {entitlement?.checkout_id && ( + {(entitlement?.license_key || entitlement?.checkout_id) && (

- {t("settings.about.proActivation.checkoutId")}:{" "} - {entitlement.checkout_id} + {t("settings.about.proActivation.licenseKey")}:{" "} + {entitlement.license_key ?? entitlement.checkout_id}

)} -
- setEmail(event.target.value)} - placeholder={t("settings.about.proActivation.emailPlaceholder")} - disabled={proSubmitting} - /> - setCheckoutId(event.target.value)} - placeholder={t( - "settings.about.proActivation.checkoutIdPlaceholder", - )} - disabled={proSubmitting} - /> -
+ setLicenseKey(event.target.value)} + placeholder={t("settings.about.proActivation.licenseKeyPlaceholder")} + disabled={proSubmitting} + /> diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index be6e8e7..1ee4dfd 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -541,14 +541,12 @@ }, "proActivation": { "active": "Dictx Pro is active on this device.", - "inactive": "Dictx Pro is not active. Enter your purchase email and checkout ID to activate updater entitlement.", + "inactive": "Dictx Pro is not active. Enter your license key to activate updater entitlement.", "activate": "Activate Dictx Pro", "refresh": "Re-check Entitlement", "clear": "Remove Activation", - "email": "Email", - "checkoutId": "Checkout ID", - "emailPlaceholder": "Purchase email", - "checkoutIdPlaceholder": "polar_cl_..." + "licenseKey": "License Key", + "licenseKeyPlaceholder": "polar_cl_..." }, "acknowledgments": { "title": "Acknowledgments", diff --git a/src/stores/proEntitlementStore.ts b/src/stores/proEntitlementStore.ts index 5c1db6e..2ff2bb3 100644 --- a/src/stores/proEntitlementStore.ts +++ b/src/stores/proEntitlementStore.ts @@ -13,7 +13,7 @@ interface ProEntitlementStore { isSubmitting: boolean; error: string | null; initialize: () => Promise; - activate: (checkoutId: string, email: string) => Promise; + activate: (licenseKey: string) => Promise; refresh: () => Promise; clear: () => Promise; } @@ -31,7 +31,7 @@ export const useProEntitlementStore = create( } try { let entitlement = await getProEntitlement(); - if (entitlement.checkout_id && entitlement.email) { + if (entitlement.license_key || entitlement.checkout_id) { try { entitlement = await refreshProEntitlement(); } catch (_error) { @@ -46,10 +46,10 @@ export const useProEntitlementStore = create( } }, - activate: async (checkoutId: string, email: string) => { + activate: async (licenseKey: string) => { set({ isSubmitting: true, error: null }); try { - const entitlement = await activateProEntitlement(checkoutId, email); + const entitlement = await activateProEntitlement(licenseKey); set({ entitlement }); return true; } catch (error) { diff --git a/src/utils/proEntitlement.ts b/src/utils/proEntitlement.ts index 20ee27b..84468a2 100644 --- a/src/utils/proEntitlement.ts +++ b/src/utils/proEntitlement.ts @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; export interface ProEntitlement { active: boolean; + license_key?: string | null; email?: string | null; checkout_id?: string | null; activated_at?: number | null; @@ -14,12 +15,10 @@ export const getProEntitlement = async (): Promise => { }; export const activateProEntitlement = async ( - checkoutId: string, - email: string, + licenseKey: string, ): Promise => { return await invoke("activate_pro_entitlement", { - checkoutId, - email, + license_key: licenseKey, }); };