Check for purchases or subscriptions
Learn how to check for purchases or subscriptions in your application to provide access to premium features.
One of the most common use cases for payments is to provide access to premium features.
Plan ID
The plan id is used to identify a plan. It is defined in the key property of the plan object from the config file like free, pro, enterprise or lifetime from the example config.
Check for purchases or subscriptions on client side
You can check if a user has a purchase or subscription by using the usePurchases hook.
The usePurchases hook returns the following properties:
activePlan: The active plan of the user, including itsstatusandtrialEndsAt(see Subscription status)purchases: An array of all the purchases of the userhasAccess: Whether the user can access the app. AlwaystruewhenrequireActiveSubscriptionisfalse. When the paywall is enabled, it is onlytrueif the active plan's status grants access.hasSubscription: A function to check if the user has an active subscription for a plan id or an array of plan ids. When the paywall is enabled, it returnsfalseif the subscription status does not grant access.hasPurchase: A function to check if the user has a specific purchase for a given plan id
function SomeComponent() {
const { activePlan, hasAccess, hasSubscription, hasPurchase } =
usePurchases();
// check if the user has a subscription for the pro plan
const hasProSubscription = hasSubscription("pro");
// or check if the user has a subscription for the pro plan or the enterprise plan
const hasProOrEnterpriseSubscription = hasSubscription(["pro", "enterprise"]);
// check if the user has a purchase of the lifetime plan
const hasLifetimeAccess = hasPurchase("lifetime");
}Check for purchases on server side
You can also check for purchases and get the active plan on the server side by utilizing the createPurchasesHelper function, which will generate a bunch of helper functions based on the purchases you pass to it.
The createPurchasesHelper can be used in both server components and API procedures, but the way to fetch the purchases is different:
Server components
When you are inside your Next.js application in a server component, you can use the listPurchases function from @payments/lib/server.
import { listPurchases } from "@payments/lib/server";
import { createPurchasesHelper } from "@repo/payments/lib/helper";
const purchases = await listPurchases();
const { activePlan, hasAccess, hasSubscription, hasPurchase } =
createPurchasesHelper(purchases);API procedures
When you are inside an oRPC procedure, you can load the purchases with the getPurchasesByUserId or getPurchasesByOrganizationId query from @repo/database.
import { getPurchasesByUserId } from "@repo/database";
import { createPurchasesHelper } from "@repo/payments/lib/helper";
import { protectedProcedure } from "../../../orpc/procedures";
export const someProcedure = protectedProcedure.handler(
async ({ context: { user } }) => {
const purchases = await getPurchasesByUserId(user.id);
const { activePlan, hasAccess, hasSubscription, hasPurchase } =
createPurchasesHelper(purchases);
// do something with the helpers here...
},
);When you load the purchases of an organization with getPurchasesByOrganizationId, verify that the user is a member of the organization first. See packages/api/modules/payments/procedures/list-purchases.ts for an example.
Check for purchases of organization
Both usePurchases and listPurchases also support organizations. Simply pass the organization id as an argument.
// ... get the organization slug from the url
const organization = await getActiveOrganization(organizationSlug);
const purchases = await listPurchases(organization.id);const { activeOrganization } = useActiveOrganization();
const { activePlan, hasAccess, hasSubscription, hasPurchase } = usePurchases(
activeOrganization!.id,
);Subscription status
Each payment provider uses its own subscription statuses. The webhook handlers map them to a shared set of statuses before saving the purchase, so your application code only has to deal with these values for activePlan.status:
| Status | Description | Grants access |
|---|---|---|
active | The subscription is paid and running | Yes |
trialing | The subscription is in its trial period | Yes |
canceling | The subscription is canceled at the end of the current billing period | Yes |
incomplete | The first payment has not completed yet | No |
past_due | A renewal payment failed and the provider is retrying | No |
unpaid | Renewal payments failed and the provider stopped retrying | No |
paused | The subscription is paused | No |
canceled | The subscription has been canceled | No |
expired | The subscription has ended | No |
One-time purchases and the implicit free plan always have the status active.
For trials, the webhooks also store the end of the trial in the trialEndsAt field of the purchase. It is available as activePlan.trialEndsAt (a Date or null) and the billing page shows when the trial ends.
The "Grants access" column only affects the app when requireActiveSubscription is enabled (see Set up a paywall). If you want to gate a feature on a healthy subscription status without enabling the paywall, use planStatusGrantsAccess:
import { planStatusGrantsAccess } from "@repo/payments/lib/subscription-status";
const canUsePremiumFeature =
activePlan?.id === "pro" && planStatusGrantsAccess(activePlan.status);The shared statuses and helpers live in packages/payments/lib/subscription-status.ts. Each provider defines how its statuses map to them in packages/payments/provider/[provider]/subscription-status.ts, so removing a provider folder also removes its mapping.