Skip to content

Require sign-in to start the desktop trial - #94

Open
leoisadev1 wants to merge 4 commits into
mainfrom
cnv-56-sign-in-trial
Open

leoisadev1 wants to merge 4 commits into
mainfrom
cnv-56-sign-in-trial

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

The desktop trial needed no account and lived in a date file, so deleting the file, pointing CONVT_DATA_DIR somewhere new or adding an OS user gave a fresh trial. Now "Start 7-day trial" signs in through the browser, and convt-billing hands the app a signed trial key: one trial per account and one per computer, checked offline against today's date. Sign-in also installs a Desktop key the account bought, and a Pro subscription in its trial unlocks the app.

sequenceDiagram
  participant App as convt app
  participant Web as convt.app (web)
  participant Billing as convt-billing
  App->>Web: browser sign-in (existing PKCE flow) → device token
  App->>Web: POST /api/device/license
  Web->>Billing: currentKey(user)
  Billing-->>App: Desktop/Pro key, a trial key while Pro is trialing, or null
  App->>Web: POST /api/device/trial {device_hash} (only if no key converts)
  Web->>Web: HMAC(DEVICE_HASH_SECRET, device_hash)
  Web->>Billing: startTrial(user, stored hash)
  Billing-->>App: plan "trial" key, or 409 device_used
Loading
  • Trial key: a new trial plan in the existing Ed25519 format. updates_until is the last trial day, judged by today, never by build date. Shipped builds reject plan:"trial" as malformed, so a trial key can't act as a paid key anywhere, and pasting one is refused.
  • Bought keys stay put: trial keys live in their own credential-store slot (trial_key, file trial.key), apart from license.key. A trial never replaces a bought key; a key the build has outgrown still runs the older builds it covers, offline, and shows again when the trial ends.
  • Device hash: HMAC of the OS machine id (or an install id kept in the credential store). Only the hash leaves the machine, and the server stores a keyed hash of that. A computer counts as used once any account's trial reached it.
  • Older apps: /api/device/license sends Desktop and trial keys only to apps that send accepts: ["desktop","pro","trial"]. Apps from before this change keep getting the account's Pro key, as they expect.
  • Pro trial: while a Pro subscription is trialing, the app gets a trial key that works through the UTC day the Pro trial ends (an end at exactly midnight keeps the day before), capped at 31 days.
  • Current users: an old trial file is carried over once with its remaining days, then never read again. Any start up to today counts.
  • Clock guard: the latest time seen and convt.app's time from each reply. A clock set back pauses a trial until an online check.
  • Packaged builds ignore CONVT_DATA_DIR, CONVT_CONFIG_DIR and CONVT_LICENSE_STORE for licensing.
  • Copy: first run, the License tab, the CLI, AGENTS.md, README, docs/plan.md and the download, contact and dashboard pages no longer say the trial needs no account. The terms and privacy pages are untouched because Replace the draft terms and privacy pages with final text #85 and feat(telemetry): report desktop and web exceptions to PostHog #82 own them; the wording they need is under Merge Danger. The soft block points at the contact page, not an address.

Evidence

Before After
Before: first run offers a trial with no account After: Start 7-day trial needs a free account
Waiting for the browser Trial on Computer already used a trial
Waiting for the browser Trial on, 7 days left Soft block: buy or contact us
Sign-in cancelled Account already owns Desktop Clock set back (offline)
Cancelled: Try again, Buy, I have a license Desktop key installed on sign-in Trial paused, Check now

The "Signed in as" line is blanked in the two green shots so no account name appears. The soft-block shot is from the current head, against a local stub of the device API; Contact us opened https://convt.app/contact.

End to end, with a source build (CONVT_LICENSE_ENFORCE=1) under Xvfb, this checkout's dev:web, convt-billing, Postgres and a headless browser sign-in with the seeded test accounts:

Scenario Result
First run → sign in → trial Free trial: 7 days left.; one trials row; stored hash = HMAC(secret, sent hash)
Cancel in the browser or the app no trial; Try again, Buy and "I have a license" shown
Second account, same computer soft block, still one trials row
Same account, fresh install same trial back (same id and last day)
Desktop-license test account signs in status shows the Desktop license, installed with no paste; convt license activate still works
Pro-trialing test account Free trial: 4 days left.; a real PNG→JPEG conversion
Trial row moved 10 days back trial-ended message, conversion exits 1, also from a build dated earlier
last-seen 3 days ahead clock message, conversion exits 1; a signed-in launch clears it
Old trial file from 2 days ago 5 days left; file removed; a rewritten file grants nothing

That run predates the later review fixes below; those are covered by the tests.

Tests, all passing on the current head:

  • cargo test (default members), cargo test -p convt-app (130), cargo test -p convt-license --features client (43 + 3 vectors, including the TS-signed trial vector), bun run rs:check
  • bun run db:ci (no drift, down file reverses, grants), packages/billing integration (110; 13 trial tests: one per account, one per computer including one that got another account's trial back, expiry, 6-way races, account deletion, currentKey rules including an outgrown Desktop key next to a Pro trial and the Pro trial's last day), apps/web unit (121) and integration (60), packages/license (20)
  • bun run check && bun run check-types && bun run build
  • New tests: a trial never overwrites a bought key, and an outgrown bought key stays stored and shows again after the trial; an older app's /license request gets no Desktop key; the app sends accepts; Start shows next to an outgrown key
  • No email address appears in the branch's diff, its commit messages or this description; test accounts are built at run time from a name and a domain

Not checked: the macOS Keychain, the Windows Credential Manager and the sandboxed Finder extension (no Linux test path); a packaged build (the Config::resolve decision is unit-tested instead). There is no trial_started / trial_expired telemetry on main (CNV-55), so none fires.

Review: an independent GPT-6.1 Sol review ran four rounds and ended at SHIP. Devin and Greptile findings since then are fixed: a failed key check no longer spends the trial; Start checks for a bought key even when signed in; a trial no longer overwrites a bought key; a computer that got an account's trial back counts as used; the Pro trial key works through the day the Pro trial ends; a legacy trial started after this build's date still carries over; an unreadable credential store can't erase carried-over days; the #[cfg(unix)] guard is back on the permissions test; older apps keep getting Pro keys from /license; a key another client stored is picked up when renewal keeps it; and the License tab offers Start 7-day trial when a bought key no longer covers this build. Not changed, by design: a client can send any device hash (a soft deterrent in AGPL software), and a successful reply's server time may lower last_seen so a clock that once ran ahead can recover.

Merge Danger

Door: one-way. Migration 0006_desktop_trial adds the trials table and devices.device_hash and grants convt_billing select (user_id, device_hash) on devices (additive; production is forward-only), and released builds will refuse to convert until a user signs in.

Blast radius: every packaged desktop and CLI user without a paid key, plus Desktop and Pro renewal. Paid keys still verify offline as before; a user mid-trial keeps their days.

Before deploying, Leo needs to:

  1. Set DEVICE_HASH_SECRET on convt-web (wrangler secret put DEVICE_HASH_SECRET, 32+ random characters, e.g. openssl rand -base64 48). Never rotate it once trials exist, or every computer can start a trial again. Until it is set, /api/device/trial answers 503.
  2. Deploy in order: bun run db:migrate against production, then convt-billing (new startTrial and currentKey RPCs; currentProKey stays), then convt-web.
  3. Ship the app only after the web deploy, since the new app calls /api/device/trial.

trials.user_id is set to null when an account is deleted, so the computer still counts as having had its trial; the row keeps only the keyed hash and dates. A user on a headless machine with only the CLI can't start a trial and needs a paid key.

Before release, the terms and privacy pages (owned by #85 and #82) still describe the old trial and need this wording:

  • terms.tsx, "Trials, licenses and subscriptions": replace "The app works fully for 7 days from your first conversion." with "The app works fully for 7 days from when you start the trial by signing in to convt.app from the app. Each account and each computer gets one app trial."
  • privacy.tsx, desktop app section: replace "The trial and your license key are stored on your computer" with "Your license key and trial key are stored on your computer".
  • privacy.tsx, "Pro renewal" bullet: rename it "License renewal" and say "If you have signed in from the app, it asks convt.app for your current license or trial key at most once a day at launch, and when you click Refresh license." Drop "Desktop license owners never sign in."
  • privacy.tsx, new bullet "Starting the trial": "The trial needs a convt.app account. When you click Start 7-day trial, the app signs you in through the browser and sends convt.app a one-way hash that identifies this computer, so each computer gets one trial. The hash is derived from your computer's ID and cannot be turned back into it, and we store only a keyed hash of it. That hash and the trial's dates stay after you delete your account, without any account details, so the computer still counts as having had its trial."

Do not merge until DEVICE_HASH_SECRET is set and the deploy order above is followed.

Fixes CNV-56

Created with Claude Opus 5.5 in T3 Code.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Devin Review

Comment thread crates/convt-app/src/account.rs
Comment thread packages/billing/src/trial.ts Outdated
Comment thread crates/convt-license/src/client.rs Outdated
Comment thread crates/convt-license/src/client.rs
Comment thread apps/web/src/server/device-auth.ts
@leoisadev1
leoisadev1 force-pushed the cnv-56-sign-in-trial branch from b549197 to 3f51e5c Compare October 7, 2026 21:45
@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5 Tier: apex

[Critical risk] Adds desktop trial activation tied to sign-in and device identity.

The reviewed changes look safe to merge once the stated deployment gates are met.

What we checked:

  • Older apps keep Pro renewal: Older apps omit accepts, so the web route still calls currentProKey. That query returns only paid Pro keys.
  • Start still checks bought keys: The new button calls start_trial. Signed-in users go through refresh_license, and an unsuccessful account-key request stops the trial flow.

Summary

The PR replaces account-free desktop trials with signed trial keys obtained through browser sign-in. Bought keys remain stored separately.

  • The latest fixes preserve Pro renewal for older apps, update kept keys in memory, and offer Start beside an outgrown paid key.
  • leoisadev1 deferred the legal-page updates to Replace the draft terms and privacy pages with final text #85 and feat(telemetry): report desktop and web exceptions to PostHog #82, with corrected wording required before release.
  • leoisadev1 accepts caller-chosen device_hash values as a soft deterrent in AGPL software. Lowering last_seen from a successful server reply is intentional so clocks can recover.
  • Keep the stated release gates: set DEVICE_HASH_SECRET, then deploy the database migration, billing, web, and app in order.
  • This review checked code and existing test assertions. Tests were not rerun; no desktop or browser was launched.

Diagram

sequenceDiagram
  participant App
  participant Web
  participant Billing
  App->>Web: Request account key
  alt App declares Desktop and trial support
    Web->>Billing: currentKey
  else Older app
    Web->>Billing: currentProKey
  end
  Billing-->>Web: Key or null
  Web-->>App: Key and server time
  opt Start clicked and no key allows conversion
    App->>Web: Trial request with device hash
    Web->>Billing: startTrial with keyed hash
    Billing-->>Web: Signed trial or device_used
    Web-->>App: Trial result
  end
Loading

Reviews (5) · Last reviewed commit: "Add a changeset for the sign-in trial" · Reviewed by Greptile

Comment thread crates/convt-license/src/client.rs Outdated
Comment thread packages/billing/src/trial.ts Outdated
Comment thread crates/convt-license/src/client.rs
Comment thread crates/convt-app/src/account.rs
Comment thread packages/billing/src/trial.ts
Comment thread packages/db/src/schema/billing.ts
Comment thread crates/convt-app/src/ui/account.rs Outdated
Comment thread crates/convt-license/src/client.rs
Comment thread crates/convt-license/src/client.rs Outdated
Comment thread packages/billing/src/trial.ts Outdated
@leoisadev1
leoisadev1 force-pushed the cnv-56-sign-in-trial branch 2 times, most recently from f6dc9fe to 95863d9 Compare October 7, 2026 22:47
Comment thread apps/web/src/server/device-routes.ts Outdated
Comment thread crates/convt-license/src/client.rs
Comment thread crates/convt-app/src/ui/account.rs Outdated
Adds the trials table, POST /api/device/trial and a currentKey RPC that
returns a Desktop or Pro key, or a trial key while Pro is trialing. A
computer counts as used once any account's trial reached it, and the
server keeps only a keyed hash of the app's device hash.
Trial keys expire by today's date and live apart from bought keys, so a
trial never replaces one. An old trial file is carried over once, a clock
set back pauses the trial until an online check, and packaged builds
ignore folder overrides for licensing state.
@leoisadev1
leoisadev1 force-pushed the cnv-56-sign-in-trial branch from 95863d9 to 66156a0 Compare October 7, 2026 23:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant