Shawn Erquhart cf5ff059a5 update version
2025-02-23 18:47:33 -05:00
wip
2024-10-31 17:35:23 -04:00
2025-02-22 16:09:08 -05:00
2025-02-23 18:47:33 -05:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
wip
2024-10-31 17:35:23 -04:00
2025-02-23 18:47:33 -05:00
wip
2025-02-23 14:17:06 -05:00
wip
2024-10-31 17:35:23 -04:00

Convex Polar Component npm version

Add subscriptions and billing to your Convex app with Polar.

// Add subscriptions to your app
const user = useQuery(api.example.getCurrentUser);

// Show available plans
<CheckoutLink
  polarApi={api.example}
  productIds={[products.premiumMonthly.id, products.premiumYearly.id]}
>
  Upgrade to Premium
</CheckoutLink>

// Manage existing subscriptions
<CustomerPortalLink polarApi={api.example}>
  Manage Subscription
</CustomerPortalLink>

Check out the example app for a complete example.

Prerequisites

Polar Account

  • Create a Polar account
  • Create an organization and generate an organization token
  • Create a product in the Polar dashboard for each pricing plan

Note: You can have one price per plan, so a plan with monthly and yearly pricing requires two products in Polar.

Note: The Convex Polar component is currently built to support recurring subscriptions, and may not work as expected with one-time payments. Please open an issue or reach out on Discord if you run into any issues.

Convex App

You'll need a Convex App to use the component. Follow any of the Convex quickstarts to set one up.

Installation

Install the component package:

npm install @convex-dev/polar

Create a convex.config.ts file in your app's convex/ folder and install the component by calling app.use:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import polar from "@convex-dev/polar/convex.config";

const app = defineApp();
app.use(polar);

export default app;

Set your Polar organization token:

npx convex env set POLAR_ORGANIZATION_TOKEN xxxxx

Usage

1. Initialize the Polar client

Create a Polar client in your Convex backend:

// convex/example.ts
import { Polar } from "@convex-dev/polar";
import { api, components } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";

export const polar = new Polar(components.polar, {
  products: {
    // Map your product keys to Polar product IDs (you can also use env vars for this)
    // Replace these with whatever your products are (eg., "pro", "pro_monthly", whatever you want)
    premiumMonthly: "product_id_from_polar",
    premiumYearly: "product_id_from_polar",
    premiumPlusMonthly: "product_id_from_polar",
    premiumPlusYearly: "product_id_from_polar",
  },
  // Provide a function the component can use to get the current user's ID and email
  getUserInfo: async (ctx) => {
    const user = await ctx.runQuery(api.example.getCurrentUser);
    return {
      userId: user._id,
      email: user.email,
    };
  },
});

// Export the API functions
export const {
  changeCurrentSubscription,
  cancelCurrentSubscription,
  getProducts,
} = polar.api();

export const {
  generateCheckoutLink,
  generateCustomerPortalUrl,
} = polar.checkoutApi();

export const _ = query(

2. Set up webhooks

The Polar component uses webhooks to keep subscription data in sync. You'll need to:

  1. Create a webhook and webhook secret in the Polar dashboard, using your Convex site URL
    • /polar/events as the webhook endpoint.
  2. Set the webhook secret in your Convex environment:
npx convex env set POLAR_WEBHOOK_SECRET xxxxx
  1. Register the webhook handler in your convex/http.ts:
import { httpRouter } from "convex/server";
import { polar } from "./example";

const http = httpRouter();

// Register the webhook handler at /polar/events
polar.registerRoutes(http as any);

export default http;

3. Display products and prices

Use the exported getProducts function to display your products and their prices:

// React component
const products = useQuery(api.example.getProducts);

// Simple example of displaying products and prices
function PricingTable() {
  const products = useQuery(api.example.getProducts);
  if (!products) return null;

  return (
    <div>
      {products.premiumMonthly && (
        <div>
          <h3>{products.premiumMonthly.name}</h3>
          <p>
            ${(products.premiumMonthly.prices[0].priceAmount ?? 0) / 100}/month
          </p>
        </div>
      )}
      {products.premiumYearly && (
        <div>
          <h3>{products.premiumYearly.name}</h3>
          <p>
            ${(products.premiumYearly.prices[0].priceAmount ?? 0) / 100}/year
          </p>
        </div>
      )}
    </div>
  );
}

Each product includes:

  • id: The Polar product ID
  • name: The product name
  • prices: Array of prices with:
    • priceAmount: Price in cents
    • priceCurrency: Currency code (e.g., "USD")
    • recurringInterval: "month" or "year"

4. Add subscription UI components

Use the provided React components to add subscription functionality to your app:

import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
import { api } from "../convex/_generated/api";

// For new subscriptions
<CheckoutLink
  polarApi={{
    generateCheckoutLink: api.example.generateCheckoutLink,
  }}
  productIds={[products.premiumMonthly.id, products.premiumYearly.id]}
>
  Upgrade to Premium
</CheckoutLink>

// For managing existing subscriptions
<CustomerPortalLink
  polarApi={{
    generateCustomerPortalUrl: api.example.generateCustomerPortalUrl,
  }}
>
  Manage Subscription
</CustomerPortalLink>

5. Handle subscription changes

The Polar component provides functions to handle subscription changes for the current user.

Note: It is highly recommended to prompt the user for confirmation before changing their subscription this way!

// Change subscription
const changeSubscription = useAction(api.example.changeCurrentSubscription);
await changeSubscription({ productId: "new_product_id" });

// Cancel subscription
const cancelSubscription = useAction(api.example.cancelCurrentSubscription);
await cancelSubscription({ revokeImmediately: true });

6. Access subscription data

Query subscription information in your app:

// convex/example.ts

// A query that returns a user with their subscription details
export const getCurrentUser = query({
  handler: async (ctx) => {
    const user = await ctx.db.query("users").first();
    if (!user) throw new Error("No user found");
    
    const subscription = await polar.getCurrentSubscription(ctx, {
      userId: user._id,
    });
    
    return {
      ...user,
      subscription,
      isFree: !subscription,
      isPremium: subscription?.productKey === "premiumMonthly" || 
                subscription?.productKey === "premiumYearly",
    };
  },
});

Example App Features

The example app demonstrates:

  • Free and paid subscription tiers
  • Monthly and yearly billing options
  • Upgrade/downgrade between plans
  • Subscription management portal
  • Usage limits based on subscription tier
  • Prorated billing for plan changes

API Reference

Polar Client

The Polar class accepts a configuration object with:

  • products: Map of product keys to Polar product IDs
  • getUserInfo: Function to get the current user's ID and email

React Components

Props:

  • polarApi: Object containing generateCheckoutLink function
  • productIds: Array of product IDs to show in the checkout
  • className: Optional CSS class name
  • children: React children (button content)

Props:

  • polarApi: Object containing generateCustomerPortalUrl function
  • className: Optional CSS class name
  • children: React children (button content)

API Functions

changeCurrentSubscription

Change an existing subscription to a new plan:

await changeSubscription({ productId: "new_product_id" });

cancelCurrentSubscription

Cancel an existing subscription:

await cancelSubscription({ revokeImmediately: true });

getCurrentSubscription

Get the current user's subscription details:

const subscription = await polar.getCurrentSubscription(ctx, { userId });

getProducts

List all available products and their prices:

const products = await polar.listProducts(ctx);

registerRoutes

Register webhook handlers for the Polar component:

polar.registerRoutes(http, {
  // Optional: customize the webhook endpoint path (defaults to "/polar/events")
  path: "/custom/webhook/path",
});

The webhook handler requires the POLAR_WEBHOOK_SECRET environment variable to be set.

Description
No description provided
Readme Apache-2.0 382 KiB
Languages
TypeScript 79.1%
JavaScript 18.2%
CSS 2.2%
HTML 0.5%