Skip to content

docs: pay-first checkout — remove sign-up wall on pricing#2870

Merged
YousefED merged 2 commits into
mainfrom
website-payment-fixes
Jun 25, 2026
Merged

docs: pay-first checkout — remove sign-up wall on pricing#2870
YousefED merged 2 commits into
mainfrom
website-payment-fixes

Conversation

@YousefED

@YousefED YousefED commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

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:

  • ~2/3 of buy intent was logged-out users hitting the sign-up wall
  • a duplicate-session bug: 13 buy-now visitors generated 130 event fires — the button looked inert during the request, so people clicked repeatedly, spawning duplicate Polar checkout sessions

What changed

docs/lib/auth.ts

  • authenticatedUsersOnly: false on the Polar checkout
  • New resolveUserForCustomer() in the subscription webhook: uses the customer externalId when 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

  • Logged-out "Buy now" goes straight to Polar checkout (no /signup redirect)
  • Loading/disabled state on the CTA + repeat-click guard → no more duplicate checkout sessions
  • try/catch around checkout() / portal(); click-pricing-signup retired (logged-out now fires click-pricing-buy-now)

How account linking stays correct

createUser triggers the Polar plugin's databaseHooks.user.create hooks, which dedupe the Polar customer by email (no duplicate) and back-fill externalId onto it — so renewals/cancellations resolve via the fast externalId path, with email reconciliation as the fallback for the first event.

Summary by CodeRabbit

  • New Features

    • Logged-out users can now complete purchases from pricing pages.
    • Pricing buttons now show a clearer “Buy now” label and indicate when checkout is in progress.
  • Bug Fixes

    • Prevents duplicate checkout or portal launches from repeated clicks.
    • Improves account matching for purchases made without signing in, reducing checkout and subscription issues.
    • Loading failures are now handled more gracefully, with retry-safe behavior.

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>
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Jun 25, 2026 6:43am
blocknote-website Ready Ready Preview Jun 25, 2026 6:43am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Pricing 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.

Changes

Pay-first pricing flow

Layer / File(s) Summary
Pricing CTA loading and gating
docs/app/pricing/tiers.tsx
Adds loading state, changes the buy label, disables repeated clicks, and updates buy and customer-portal request handling with error reporting and disabled link behavior.
Polar pay-first user resolution
docs/lib/auth.ts
Allows logged-out checkout, resolves subscription users from customer data or checkout email, creates missing users, and sends a magic-link sign-in when needed.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through pricing with a loading glow,
No double-click burrows left to grow.
Polar sent the checkout breeze,
Webhooks hummed among the trees,
And magic links danced home with ease.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed Mostly matches the template with summary, rationale, changes, and impact details; testing and screenshots sections are the main omissions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: enabling pay-first checkout on pricing and removing the sign-up gate.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch website-payment-fixes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://TypeCellOS.github.io/BlockNote/pr-preview/pr-2870/

Built to branch gh-pages at 2026-06-25 06:51 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2870

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2870

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2870

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2870

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2870

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2870

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2870

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2870

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2870

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2870

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2870

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2870

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2870

commit: 740d61c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between ade62f2 and 0f697dc.

📒 Files selected for processing (2)
  • docs/app/pricing/tiers.tsx
  • docs/lib/auth.ts

}

if (session.planType === "free") {
track("Signup", { tier: tier.id });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ 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.

Comment thread docs/lib/auth.ts
Comment on lines +236 to +245
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ 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.

Comment thread docs/lib/auth.ts
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>
@YousefED YousefED changed the title feat(docs): pay-first checkout — remove sign-up wall on pricing docs: pay-first checkout — remove sign-up wall on pricing Jun 25, 2026
@YousefED YousefED merged commit 47f5a3b into main Jun 25, 2026
35 checks passed
@YousefED YousefED deleted the website-payment-fixes branch June 25, 2026 06:47
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