Entitlements

Show it in your frontend

Show end users the plans they can choose, what each grants and what is left of their allowance, with the PricingTable and UsageMeter React components.

Your backend decides what an end user may do. Your frontend shows them what they have: which plan they are on, what each plan grants, and how much of the allowance is left. Two components of @shiprev/react do that, PricingTable and UsageMeter. They display only; a ruling is always made in your backend.

This page covers version 0.1.0-beta.0, installed with npm install @shiprev/react@beta. It assumes the provider and the forwarding path are set up as described in The React components; both components read with the signed-in end user's session.

What the end user has: UsageMeter

UsageMeter reads the buyer organization's usage of the current period and shows, for each capability, what is used out of the allowance, with a bar.

Usage.tsx
import { UsageMeter } from '@shiprev/react';

export function Usage() {
  return <UsageMeter capabilityKeys={['report_exports', 'seats']} showWalletBalance />;
}
PropWhat it does
capabilityKeysShows only these capabilities, in this order. Without it, every capability of the organization is shown
showWalletBalanceAlso shows the organization's wallet balance. Off by default
summaryA usage summary you already have; the component then does not read it

The figures come from GET /public/v1/usage/summary, which answers from the buyer organization's entitlements at your application's region. They are the same figures your backend rules on, and they move when reported usage has been counted. When an allowance is not a positive number, only the amount used is shown, without a bar.

What the plans grant: PricingTable

PricingTable lists your published plans, add-ons excepted, with the price of their latest version and what each grants, one line per capability with its allowance. The organization's current plan is marked, and the end user can choose another one.

Plans.tsx
import { PricingTable } from '@shiprev/react';

export function Plans() {
  return <PricingTable successUrl="/billing?done=1" cancelUrl="/billing" />;
}

What choosing a plan does depends on the organization's subscription:

The organizationChoosing a plan
Has no active subscriptionOpens checkout for that plan version; checkout returns to successUrl or cancelUrl, the current page by default
Has an active subscriptionAsks the end user to confirm, then changes the plan. The change takes effect at once, or opens a payment page when the region asks for one
PropWhat it does
successUrl, cancelUrlWhere checkout returns
onSelectPlanTakes over the choice: the component calls it with the plan version and 'subscribe' or 'change', and sends no request itself
onPlanChangedCalled with the new subscription after a change took effect
onNavigateOpens the checkout and payment pages; window.location.assign by default
plans, subscriptionData you already have; the component then does not read it

Once a change has taken effect, the region pushes the new entitlements to your backend, and the next ruling uses them; see Snapshot, mirror and offline.

When the allowance runs out

When your backend refuses a call with quota_exhausted, tell the frontend so, and show the plans there. With Overage policies, Prompt to upgrade names exactly this intent; the component does the same for any policy.

ExportButton.tsx
import { useState } from 'react';
import { PricingTable, UsageMeter } from '@shiprev/react';

export function ExportButton() {
  const [limitReached, setLimitReached] = useState(false);

  async function exportReport() {
    const response = await fetch('/api/reports/export', { method: 'POST', headers: authHeaders() });
    if (response.status === 403 && (await response.json()).reason === 'quota_exhausted') setLimitReached(true);
  }

  return limitReached ? (
    <>
      <UsageMeter capabilityKeys={['report_exports']} />
      <PricingTable />
    </>
  ) : (
    <button onClick={exportReport}>Export</button>
  );
}

Your backend has to put the reason in its answer for this to work; Protect routes shows how.

To hide a feature the plan does not include rather than refuse it, let your backend tell the frontend which features are granted, from rulings with quantity: 0; see Check a capability without using it. Hiding is for display only: keep the ruling on the route that does the work.

Labels and errors

Both components take labels, to replace any of their English texts, className, added to their root element, and onError, called once for each failed request while the component shows its own error state. A signed-out end user sees a prompt to sign in.

Where to go from here

© 2026 ShipRev