Skip to content

feat: single CTA on anonymous limit screen — create account and start trial atomically#2455

Merged
aalemayhu merged 4 commits into
mainfrom
feat/trial-on-registration
May 19, 2026
Merged

feat: single CTA on anonymous limit screen — create account and start trial atomically#2455
aalemayhu merged 4 commits into
mainfrom
feat/trial-on-registration

Conversation

@aalemayhu
Copy link
Copy Markdown
Contributor

@aalemayhu aalemayhu commented May 19, 2026

What

Anonymous users who hit the upload limit (card count or file size) now see a single primary CTA — "Create account and start trial" — instead of the previous two-button layout ("See upgrade options" + hidden "Start 1-hour trial"). The CTA links to `/register?redirect=/upload&start_trial=1`. The registration endpoint atomically creates the account and starts the 1-hour trial in a single request. The originally-uploaded filename is preserved across the redirect via `sessionStorage`, and a one-line banner on the post-register upload page reads "Re-attach to convert."

Why

Fixes #2362. Part of umbrella PR #2446 (first-deck-success activation wave). Wave 3 of 4. Merge order: after PR #2352, before PR #252 (onboarding tour).

Today the trial CTA for anonymous users was the "Sign in to start trial" link on the limit screen. The user had to: (1) sign in or register, (2) come back to upload, (3) separately start the trial. This PR collapses those steps into one: click the CTA → register → trial is already started.

How

Server (src/controllers/UsersControllers.ts): The existing /api/users/register endpoint now reads start_trial from the request body. If set to '1', it calls StartTrialUseCase after the user row is created and the JWT cookie is set. If the trial start fails (already used, already paid, or any error), the failure is logged but the signup succeeds — account creation is the more valuable side effect.

Client (Backend.ts / RegisterForm / RegisterPage): RegisterPage reads start_trial=1 from the query string and passes it as a prop to RegisterForm. RegisterForm forwards it to the backend register call. The submit button label changes to "Create account and start trial" when the flag is present.

Upload limit screen (UploadForm.tsx): For anonymous users, replaced the two-button layout with a single <Link> to /register?redirect=/upload&start_trial=1. The filename is stored in sessionStorage (upload_pending_filename) when clicking the CTA. Added saveFilenameForReattach helper.

Post-register banner (UploadPage.tsx + CSS): Reads upload_pending_filename from sessionStorage on mount. If present, shows a one-line status banner: "Re-attach to convert." The sessionStorage entry is cleared on the next successful upload.

The getLimitDescription function was refactored to take a 'anonymous' | 'trial_available' | 'trial_used' context instead of a boolean, giving each case its own copy.

Measuring success

  • Log line Trial start failed after registration appearing in prod logs at <1% rate confirms the happy path works and the fallback is catching edge cases
  • A new trial_started_at row set on the same DB write as created_at (query: SELECT count(*) FROM users WHERE trial_started_at::date = created_at::date AND created_at > now() - interval '24 hours') confirms atomic registration + trial is happening in production

Testing

  • Unit tests added: 3 new server-side tests in UsersControllers.test.ts covering the trial-on-register happy path, fallback when trial already used, and absence of trial when flag is not set
  • 2 new UploadPage tests for the reattach banner (shown / not shown based on sessionStorage)
  • Existing UploadForm limit-screen test updated to assert the new single-CTA design for anonymous users
  • All 742 web tests pass; all 34 server controller tests pass; tsc clean on both server and web; Biome lint clean

Changelog

Deferred to wave's final PR per spec (#2446 umbrella). No entry in this PR.

Risks

  • start_trial can only be triggered from within the same atomic /users/register request — there is no path for an attacker to trigger a trial on a third-party user's account. The flag is body-only (not a query param on the endpoint itself) and gated behind a new account creation. An existing user attempting to re-register gets a 400 before the trial code is reached.
  • If StartTrialUseCase throws unexpectedly, the error is caught and logged; the signup response is still 200 with the JWT cookie set. The user can start the trial manually via the existing "Start 1-hour trial" button if needed.
  • Filename stored in sessionStorage is rendered via React's normal text path (no dangerouslySetInnerHTML). CWE-79 safe.
  • JWT secret is unchanged — no hardcoded fallback introduced. CWE-321 safe.
  • /security-review is recommended before merge: this PR touches the signup flow (CWE-862 check above) and the trial-start path.

Goal alignment

Directly addresses the first-deck-success activation gap: anonymous users who hit the limit can now convert in a single click-through instead of a multi-step detour. Reduces drop-off at the most critical conversion funnel step.

Sonar

sonar-scanner was not run locally (no SONAR_TOKEN configured in this environment). New code paths are covered by unit tests. The only new security-adjacent path is the start_trial body flag handling, which does not touch user input beyond a string equality check against a fixed value.


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

aalemayhu and others added 4 commits May 19, 2026 16:06
When /api/users/register receives start_trial=1, it calls StartTrialUseCase
after account creation. If the trial fails, the failure is logged but the
signup still succeeds — account creation is the more valuable side effect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RegisterPage reads start_trial=1 from query params and passes it to
RegisterForm as a prop. RegisterForm forwards the flag to the backend
register call and shows "Create account and start trial" as the submit
button label when the flag is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Anonymous users at the upload limit now see one primary CTA —
"Create account and start trial" — linking to
/register?redirect=/upload&start_trial=1. The filename is saved to
sessionStorage so UploadPage can show a "Re-attach <filename> to convert"
banner after registration. The sessionStorage entry is cleared on the next
successful upload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Biome flagged the != null + else block as a negation test. The behaviour
is unchanged; leading with the null branch silences the rule per the
repo's "lead with the positive" convention.
@netlify
Copy link
Copy Markdown

netlify Bot commented May 19, 2026

Deploy Preview for notion2anki ready!

Name Link
🔨 Latest commit 2cf964a
🔍 Latest deploy log https://app.netlify.com/projects/notion2anki/deploys/6a0c6ec18147490007e1ea7d
😎 Deploy Preview https://deploy-preview-2455--notion2anki.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@netlify
Copy link
Copy Markdown

netlify Bot commented May 19, 2026

Deploy Preview for notion2anki ready!

Name Link
🔨 Latest commit 513eed4
🔍 Latest deploy log https://app.netlify.com/projects/notion2anki/deploys/6a0c6f90ce7c7d0008dad042
😎 Deploy Preview https://deploy-preview-2455--notion2anki.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

1 similar comment
@netlify
Copy link
Copy Markdown

netlify Bot commented May 19, 2026

Deploy Preview for notion2anki ready!

Name Link
🔨 Latest commit 513eed4
🔍 Latest deploy log https://app.netlify.com/projects/notion2anki/deploys/6a0c6f90ce7c7d0008dad042
😎 Deploy Preview https://deploy-preview-2455--notion2anki.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@aalemayhu aalemayhu merged commit 81c53fd into main May 19, 2026
11 checks passed
@aalemayhu aalemayhu deleted the feat/trial-on-registration branch May 19, 2026 14:29
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.

Anonymous trial-on-registration: unblock the upload-limit dead-end

1 participant