feat: single CTA on anonymous limit screen — create account and start trial atomically#2455
Merged
Conversation
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.
✅ Deploy Preview for notion2anki ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
❌ The last analysis has failed. |
✅ Deploy Preview for notion2anki ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
1 similar comment
✅ Deploy Preview for notion2anki ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/registerendpoint now readsstart_trialfrom the request body. If set to'1', it callsStartTrialUseCaseafter 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):RegisterPagereadsstart_trial=1from the query string and passes it as a prop toRegisterForm.RegisterFormforwards it to the backendregistercall. 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 insessionStorage(upload_pending_filename) when clicking the CTA. AddedsaveFilenameForReattachhelper.Post-register banner (
UploadPage.tsx+ CSS): Readsupload_pending_filenamefromsessionStorageon mount. If present, shows a one-line status banner: "Re-attach to convert." The sessionStorage entry is cleared on the next successful upload.The
getLimitDescriptionfunction was refactored to take a'anonymous' | 'trial_available' | 'trial_used'context instead of a boolean, giving each case its own copy.Measuring success
Trial start failed after registrationappearing in prod logs at <1% rate confirms the happy path works and the fallback is catching edge casestrial_started_atrow set on the same DB write ascreated_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 productionTesting
UsersControllers.test.tscovering the trial-on-register happy path, fallback when trial already used, and absence of trial when flag is not setChangelog
Deferred to wave's final PR per spec (#2446 umbrella). No entry in this PR.
Risks
start_trialcan only be triggered from within the same atomic/users/registerrequest — 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.StartTrialUseCasethrows 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.sessionStorageis rendered via React's normal text path (nodangerouslySetInnerHTML). CWE-79 safe./security-reviewis 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-scannerwas not run locally (noSONAR_TOKENconfigured in this environment). New code paths are covered by unit tests. The only new security-adjacent path is thestart_trialbody flag handling, which does not touch user input beyond a string equality check against a fixed value.Need help on this PR? Tag
@codesmithwith what you need.