Skip to content

docs: add yearly pricing option with discount#2597

Merged
YousefED merged 1 commit intomainfrom
docs/pricing-test
Mar 27, 2026
Merged

docs: add yearly pricing option with discount#2597
YousefED merged 1 commit intomainfrom
docs/pricing-test

Conversation

@YousefED
Copy link
Copy Markdown
Collaborator

@YousefED YousefED commented Mar 27, 2026

Summary

  • Add monthly/yearly toggle to the pricing page (defaults to yearly)
  • Yearly plan: $195/mo ($2,340/yr) — 50% off the $390/mo monthly price
  • Wire up business-yearly Polar product for checkout
  • Business monthly and yearly treated as the same tier for subscription management

Summary by CodeRabbit

  • New Features

    • Added an interactive toggle to switch between monthly and yearly billing frequency on the pricing page
    • Yearly billing plans now display with calculated monthly cost equivalents, reference pricing, and a savings indicator badge
  • Updates

    • Corrected the Business plan's yearly pricing calculation

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

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

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Mar 27, 2026 10:56am
blocknote-website Ready Ready Preview Mar 27, 2026 10:56am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a pricing frequency toggle feature. A new PricingTiers wrapper component manages monthly/yearly state and controls tier rendering. The tiers component extends to support yearly pricing display with adjusted pricing calculations. Product configuration is updated to include a new business-yearly pricing tier option.

Changes

Cohort / File(s) Summary
Pricing UI Components
docs/app/pricing/PricingTiers.tsx, docs/app/pricing/tiers.tsx
New PricingTiers wrapper component with frequency toggle state ("month" | "year"). Updated Tiers component to accept frequency prop, render yearly pricing with monthly equivalent calculation and "-50%" badge, and pass frequency to CTA button. Added business plan detection logic for "Manage subscription" vs "Update subscription" text.
Pricing Page Integration
docs/app/pricing/page.tsx
Updated to use new PricingTiers wrapper instead of directly rendering Tiers. Modified Business plan yearly price from 48 to 2340.
Product Configuration
docs/lib/auth.ts, docs/lib/product-list.ts
Added business-yearly product entry to checkout configuration. Defined new PRODUCTS["business-yearly"] constant with environment-specific product ID, name, and slug; updated ProductSlug type to include "business-yearly".

Sequence Diagram

sequenceDiagram
    actor User
    participant PricingTiers
    participant Tiers
    participant TierCTAButton
    participant Checkout

    User->>PricingTiers: Load pricing page
    PricingTiers->>PricingTiers: Initialize frequency state (default "year")
    PricingTiers->>Tiers: Render with tiers + frequency
    
    User->>PricingTiers: Toggle frequency switch
    PricingTiers->>PricingTiers: Update frequency state
    PricingTiers->>Tiers: Re-render with new frequency
    
    Tiers->>Tiers: Render pricing display (yearly or monthly)
    alt frequency === "year"
        Tiers->>Tiers: Show monthly equivalent, strikethrough price, "-50%" badge
    else frequency === "month"
        Tiers->>Tiers: Show standard monthly pricing
    end
    
    Tiers->>TierCTAButton: Pass frequency prop
    User->>TierCTAButton: Click purchase button
    TierCTAButton->>TierCTAButton: Determine checkout slug based on frequency
    alt business tier + yearly
        TierCTAButton->>Checkout: Use "business-yearly" slug
    else other tiers
        TierCTAButton->>Checkout: Use tier.id slug
    end
    TierCTAButton->>Checkout: Redirect to checkout
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A toggle switches left and right,
Yearly gains its moonlit light,
Prices dance with fifty's grace,
Business tiers find their place!
Hop along with us in cheer,
Billing's now both bright and clear! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description covers Summary, Rationale (implicit in the bullet points), and Changes, but lacks Testing, Screenshots, and Checklist sections from the template.
Title check ✅ Passed The title 'docs: add yearly pricing option with discount' accurately summarizes the main change: introducing a yearly pricing option with a discount to the pricing page.

✏️ 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 docs/pricing-test

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 and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 27, 2026

Open in StackBlitz

@blocknote/ariakit

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

@blocknote/code-block

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

@blocknote/core

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

@blocknote/mantine

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

@blocknote/react

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

@blocknote/server-util

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

@blocknote/shadcn

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

@blocknote/xl-ai

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

@blocknote/xl-docx-exporter

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

@blocknote/xl-email-exporter

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

@blocknote/xl-multi-column

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

@blocknote/xl-odt-exporter

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

@blocknote/xl-pdf-exporter

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

commit: b2f7cbf

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
docs/app/pricing/tiers.tsx (1)

97-100: Include billing frequency in the pricing events.

This branch adds a second Business checkout path, but the surrounding telemetry still only sends tier: tier.id. Monthly and yearly clicks will collapse into the same funnel, which makes it harder to validate adoption of the new yearly offer. Add frequency or checkoutSlug to the event payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/app/pricing/tiers.tsx` around lines 97 - 100, The telemetry currently
only sends tier: tier.id so monthly and yearly Business checkouts collapse;
update the pricing click/purchase event payload to include the billing frequency
or the computed checkoutSlug (use the existing checkoutSlug variable or
frequency) before calling authClient.checkout so events distinguish yearly vs
monthly (locate the code that builds the event payload which currently
references tier.id and add frequency or checkoutSlug to that payload).
docs/lib/auth.ts (1)

210-213: Optional: Normalize yearly billing back to the existing Business entitlement.

The webhook at docs/lib/auth.ts lines 240-244 persists the product slug directly as planType. While this means planType can be either "business" or "business-yearly", the codebase already handles both values correctly through the BUSINESS_PLAN_TYPES set and the isBusinessPlan() helper function in docs/app/pricing/tiers.tsx.

However, normalizing planType back to "business" in the webhook and storing billing cadence separately would improve clarity and reduce semantic ambiguity in the stored data. The current approach works but is less explicit about the intent.

Suggested normalization in the webhook update path
-                  const planType = Object.values(PRODUCTS).find(
+                  const productSlug = Object.values(PRODUCTS).find(
                     (p) => p.id === productId,
                   )?.slug;
+                  const planType =
+                    productSlug === "business-yearly" ? "business" : productSlug;
                   await authContext.internalAdapter.updateUser(userId, {
                     planType,
                   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/auth.ts` around lines 210 - 213, The webhook in docs/lib/auth.ts
persists the product slug directly into planType (e.g., "business-yearly");
change the update path so that if the selected product slug is "business-yearly"
you normalize planType to "business" and persist billing cadence separately
(e.g., billingInterval = "yearly" vs "monthly"), otherwise keep planType as the
slug for non-business plans; use the existing PRODUCTS mapping and ensure
compatibility with BUSINESS_PLAN_TYPES and isBusinessPlan() by storing only the
canonical "business" plan in planType and recording cadence metadata alongside
it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/app/pricing/PricingTiers.tsx`:
- Around line 24-34: The billing toggle button with role="switch" and
aria-checked (in the component using frequency and setFrequency) lacks an
accessible name; update the button to include an accessible label by either
adding aria-label="Billing frequency toggle" or by giving the visible
“Monthly/Yearly” text an id and wiring aria-labelledby="<that-id>" on the button
so screen readers announce the switch state; keep the existing frequency and
setFrequency handlers intact and ensure the chosen id is unique in the
component.

In `@docs/lib/product-list.ts`:
- Around line 10-17: The "business-yearly" product entry uses the placeholder ID
"NOT-CREATED" in non-production, which breaks sandbox yearly checkout flows;
either replace that placeholder with the actual sandbox Polar product ID for the
yearly plan or gate/disable the yearly option outside production where
"business-yearly" is referenced (e.g., in docs/app/pricing/PricingTiers.tsx
default selection and in docs/lib/auth.ts checkout registration). Update the
"business-yearly" object to contain the sandbox product ID when NODE_ENV !==
"production", or add an environment check in PricingTiers.tsx/auth.ts to hide or
disable the yearly option when the ID is missing.

---

Nitpick comments:
In `@docs/app/pricing/tiers.tsx`:
- Around line 97-100: The telemetry currently only sends tier: tier.id so
monthly and yearly Business checkouts collapse; update the pricing
click/purchase event payload to include the billing frequency or the computed
checkoutSlug (use the existing checkoutSlug variable or frequency) before
calling authClient.checkout so events distinguish yearly vs monthly (locate the
code that builds the event payload which currently references tier.id and add
frequency or checkoutSlug to that payload).

In `@docs/lib/auth.ts`:
- Around line 210-213: The webhook in docs/lib/auth.ts persists the product slug
directly into planType (e.g., "business-yearly"); change the update path so that
if the selected product slug is "business-yearly" you normalize planType to
"business" and persist billing cadence separately (e.g., billingInterval =
"yearly" vs "monthly"), otherwise keep planType as the slug for non-business
plans; use the existing PRODUCTS mapping and ensure compatibility with
BUSINESS_PLAN_TYPES and isBusinessPlan() by storing only the canonical
"business" plan in planType and recording cadence metadata alongside it.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: bffda504-dbaf-4074-a6e2-4f58153e5b0a

📥 Commits

Reviewing files that changed from the base of the PR and between cd92dc2 and b2f7cbf.

📒 Files selected for processing (5)
  • docs/app/pricing/PricingTiers.tsx
  • docs/app/pricing/page.tsx
  • docs/app/pricing/tiers.tsx
  • docs/lib/auth.ts
  • docs/lib/product-list.ts

Comment on lines +24 to +34
<button
type="button"
role="switch"
aria-checked={frequency === "year"}
onClick={() =>
setFrequency((f) => (f === "month" ? "year" : "month"))
}
className={cn(
"relative inline-flex h-7 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
frequency === "year" ? "bg-purple-600" : "bg-stone-300",
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Give the billing switch a programmatic label.

The control has role="switch" and aria-checked, but it still has no accessible name, so screen readers will announce an unnamed switch. Add aria-label or wire aria-labelledby to the visible “Monthly/Yearly” text.

Accessibility fix
         <button
           type="button"
           role="switch"
+          aria-label="Toggle billing frequency"
           aria-checked={frequency === "year"}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type="button"
role="switch"
aria-checked={frequency === "year"}
onClick={() =>
setFrequency((f) => (f === "month" ? "year" : "month"))
}
className={cn(
"relative inline-flex h-7 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
frequency === "year" ? "bg-purple-600" : "bg-stone-300",
)}
<button
type="button"
role="switch"
aria-label="Toggle billing frequency"
aria-checked={frequency === "year"}
onClick={() =>
setFrequency((f) => (f === "month" ? "year" : "month"))
}
className={cn(
"relative inline-flex h-7 w-12 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
frequency === "year" ? "bg-purple-600" : "bg-stone-300",
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/app/pricing/PricingTiers.tsx` around lines 24 - 34, The billing toggle
button with role="switch" and aria-checked (in the component using frequency and
setFrequency) lacks an accessible name; update the button to include an
accessible label by either adding aria-label="Billing frequency toggle" or by
giving the visible “Monthly/Yearly” text an id and wiring
aria-labelledby="<that-id>" on the button so screen readers announce the switch
state; keep the existing frequency and setFrequency handlers intact and ensure
the chosen id is unique in the component.

Comment on lines +10 to +17
"business-yearly": {
id:
process.env.NODE_ENV === "production"
? "ba3965dc-e1ca-494e-b36a-62e2e41615d4"
: "NOT-CREATED",
name: "Business Yearly",
slug: "business-yearly",
} as const,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expected: a placeholder non-production id in product-list,
# the slug registered in auth checkout config,
# and the pricing UI defaulting to yearly.
rg -n -C2 '"business-yearly"|NOT-CREATED' docs/lib/product-list.ts docs/lib/auth.ts
rg -n -C2 'useState<Frequency>\("year"\)' docs/app/pricing/PricingTiers.tsx

Repository: TypeCellOS/BlockNote

Length of output: 1089


Add sandbox product ID for non-production yearly flow or gate yearly outside production.

The yearly plan defaults in the UI (docs/app/pricing/PricingTiers.tsx, line 10) and is registered in checkout (docs/lib/auth.ts, lines 211–212), but uses the placeholder "NOT-CREATED" for non-production environments (docs/lib/product-list.ts, line 14). This prevents end-to-end testing of the yearly checkout flow in sandbox. Either create the missing Polar sandbox product and populate its ID, or disable the yearly option outside production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/product-list.ts` around lines 10 - 17, The "business-yearly" product
entry uses the placeholder ID "NOT-CREATED" in non-production, which breaks
sandbox yearly checkout flows; either replace that placeholder with the actual
sandbox Polar product ID for the yearly plan or gate/disable the yearly option
outside production where "business-yearly" is referenced (e.g., in
docs/app/pricing/PricingTiers.tsx default selection and in docs/lib/auth.ts
checkout registration). Update the "business-yearly" object to contain the
sandbox product ID when NODE_ENV !== "production", or add an environment check
in PricingTiers.tsx/auth.ts to hide or disable the yearly option when the ID is
missing.

@YousefED YousefED changed the title Add yearly pricing option with 50% discount docs: add yearly pricing option with discount Mar 27, 2026
@YousefED YousefED merged commit a850078 into main Mar 27, 2026
34 checks passed
@YousefED YousefED deleted the docs/pricing-test branch March 27, 2026 12:12
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