docs: pay-first checkout — remove sign-up wall on pricing#2870
Conversation
Logged-out buyers now go straight to Polar checkout instead of being sent to /signup first. The purchase is reconciled to a BlockNote account by email in the Polar webhook (creating the account if needed), and the buyer is emailed a magic sign-in link to access their plan. Also adds a loading/disabled state to the pricing CTAs so repeat clicks can't open duplicate Polar checkout sessions while a request is in flight. Motivated by funnel analysis: of ~9.2k quarterly /pricing visitors only ~0.36% clicked a buy CTA, two-thirds of buy intent bounced off the sign-up wall, and an unresponsive button produced duplicate checkout sessions (13 visitors -> 130 buy-now fires). - lib/auth.ts: authenticatedUsersOnly:false; resolveUserForCustomer() reconciles by externalId or email; signInMagicLink for access. - app/pricing/tiers.tsx: logged-out -> checkout; loading guard + try/catch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughPricing CTAs now manage loading state, prevent duplicate buy or portal requests, and update link behavior during pay-first flows. Polar checkout accepts logged-out buyers, and subscription webhooks resolve users by customer ID or email before updating plan type. ChangesPay-first pricing flow
Sequence Diagram(s)sequenceDiagram
participant TierCTAButton
participant AuthClientCheckout as authClient.checkout
participant Polar
participant PolarWebhook as Polar webhook onPayload
participant ResolveUser as resolveUserForCustomer
participant MagicLink as auth.api.signInMagicLink
participant Sentry
TierCTAButton->>AuthClientCheckout: submit buy or portal request
AuthClientCheckout->>Polar: create pay-first checkout
Polar-->>PolarWebhook: subscription event
PolarWebhook->>ResolveUser: resolve userId
ResolveUser->>MagicLink: send magic-link sign-in
ResolveUser->>Sentry: capture mail failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/mantine
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/app/pricing/tiers.tsx`:
- Line 93: The pricing CTA handler still fires the retired Signup analytics
event in the shared click path, so both purchase and portal-management actions
are being misclassified. Update the handler in tiers.tsx around the CTA tracking
logic to remove track("Signup", { tier: tier.id }) from the buy/manage flow, or
move it into a true signup-only branch that only runs for actual registration
actions. Keep the existing click-pricing-buy-now and related event tracking in
the pay-first and portal-management branches unchanged.
In `@docs/lib/auth.ts`:
- Around line 236-245: The paid-subscription webhook path in
resolveUserForCustomer handling currently returns early when no userId is found,
which silently drops unresolved purchases. Update the auth.ts flow around
resolveUserForCustomer/auth.$context to either record a non-PII Sentry
error/message before returning or throw an error so the webhook can be retried,
while preserving the existing customer resolution logic.
- Around line 327-349: The new-user path in resolveUserForCustomer creates a
BlockNote user but never links it back to the Polar customer record. After
authContext.internalAdapter.createUser succeeds, update the Polar customer using
the new created.id as userId before returning so the webhook customer record is
tied to the newly provisioned account. Keep the existing signInMagicLink flow
and error handling, but ensure the customer linkage happens immediately in
resolveUserForCustomer for the no-existing-user branch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 76cb21f7-b762-4311-abe3-03f0c24859ec
📒 Files selected for processing (2)
docs/app/pricing/tiers.tsxdocs/lib/auth.ts
| } | ||
|
|
||
| if (session.planType === "free") { | ||
| track("Signup", { tier: tier.id }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Remove the retired Signup analytics event for buy CTAs.
Line 93 still fires before both pay-first checkout and portal-management branches, so logged-out purchases now record both Signup and click-pricing-buy-now, while signed-in manage/update clicks are also counted as signups. Delete it or move it to a true signup-only path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/app/pricing/tiers.tsx` at line 93, The pricing CTA handler still fires
the retired Signup analytics event in the shared click path, so both purchase
and portal-management actions are being misclassified. Update the handler in
tiers.tsx around the CTA tracking logic to remove track("Signup", { tier:
tier.id }) from the buy/manage flow, or move it into a true signup-only branch
that only runs for actual registration actions. Keep the existing
click-pricing-buy-now and related event tracking in the pay-first and
portal-management branches unchanged.
| // Resolve the BlockNote account for this purchase. For pay-first | ||
| // (logged-out) checkouts the customer has no externalId, so this | ||
| // creates/links an account by email and sends a sign-in link. | ||
| const userId = await resolveUserForCustomer( | ||
| payload.data.customer, | ||
| ); | ||
| if (!userId) { | ||
| return; | ||
| } | ||
| const authContext = await auth.$context; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Don’t silently acknowledge unresolved paid subscriptions.
If resolveUserForCustomer returns null, the webhook exits successfully and the buyer’s paid plan is never provisioned or alerted on. Capture a non-PII Sentry error/message before returning, or throw if this should be retried.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/lib/auth.ts` around lines 236 - 245, The paid-subscription webhook path
in resolveUserForCustomer handling currently returns early when no userId is
found, which silently drops unresolved purchases. Update the auth.ts flow around
resolveUserForCustomer/auth.$context to either record a non-PII Sentry
error/message before returning or throw an error so the webhook can be retried,
while preserving the existing customer resolution logic.
A logged-out checkout creates a Polar customer with no externalId. The webhook created the BlockNote account but relied on the Polar plugin's implicit user.create hook to back-fill externalId — which only runs when an auth context is present during the webhook, so the link wasn't guaranteed. Without it, customer.portal() and subscription lookups (which resolve by externalId === user.id) can't find the buyer's subscription. Add linkPolarCustomer() and call it explicitly after resolving/creating the user (created, existing, and race paths). Best-effort so a Polar API hiccup doesn't block provisioning the plan; subsequent subscription events retry the link via the same path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Enables pay-first checkout: logged-out visitors can buy a plan without first creating an account. Previously the "Buy" CTA sent logged-out users to
/signup, where most dropped off.Why
Funnel analysis showed a conversion leak:
What changed
docs/lib/auth.tsauthenticatedUsersOnly: falseon the Polar checkoutresolveUserForCustomer()in the subscription webhook: uses the customerexternalIdwhen present; otherwise reconciles by checkout email — finds or creates the BlockNote account so the plan can be provisioned, then emails a magic sign-in link (auth.api.signInMagicLink) so the buyer can access it. Handles the concurrent-webhook race (unique email → re-fetch).docs/app/pricing/tiers.tsx/signupredirect)try/catcharoundcheckout()/portal();click-pricing-signupretired (logged-out now firesclick-pricing-buy-now)How account linking stays correct
createUsertriggers the Polar plugin'sdatabaseHooks.user.createhooks, which dedupe the Polar customer by email (no duplicate) and back-fillexternalIdonto it — so renewals/cancellations resolve via the fastexternalIdpath, with email reconciliation as the fallback for the first event.Summary by CodeRabbit
New Features
Bug Fixes