useSubscription
The plans, the billing cycle and the usage bars behind the pricing and profile pages. Demo state, shaped so a real billing provider can replace it.
Import
ts
import {
acceptedCards,
annualPrice,
ANNUAL_DISCOUNT,
formatCadence,
formatPrice,
paymentMethod,
useSubscription,
} from '@shared/composables/useSubscription'Signature
ts
function useSubscription(): {
plans: Ref<Plan[]>
currentPlan: ComputedRef<Plan>
currentPlanId: Ref<string>
billingCycle: Ref<BillingCycle>
usage: { seatsUsed: number, seats: number, storageUsedGb: number, storageGb: number }
seatPercent: ComputedRef<number>
storagePercent: ComputedRef<number>
currentPrice: ComputedRef<string>
currentCadence: ComputedRef<string>
selectPlan: (id: string) => void
addPlan: (plan: Plan) => void
}BillingCycle is 'monthly' | 'annually'.
Returns
| Member | Type | Description |
|---|---|---|
plans | Ref<Plan[]> | The four shipped tiers, plus anything added. |
currentPlan | ComputedRef<Plan> | The selected plan, falling back to the first. |
currentPlanId | Ref<string> | The id. Starts at 'starter'. |
billingCycle | Ref<BillingCycle> | Monthly or annually. |
usage | object | Seats and storage, used and available. |
seatPercent / storagePercent | ComputedRef<number> | Rounded percentages for the bars. |
currentPrice | ComputedRef<string> | The headline figure in the selected cycle. |
currentCadence | ComputedRef<string> | The line under it. |
selectPlan | (id) => void | Switches plan. An unknown id is ignored. |
addPlan | (plan) => void | Appends a tier. |
Plan
| Key | Type | Description |
|---|---|---|
id | string | Unique. |
name | string | A translation key or a literal. |
priceMonthly | number | null | USD per month. null means quoted rather than priced. |
blurb | string | The line under the name. |
features | string[] | The bullet list. |
highlight | boolean | Drawn as the recommended tier. One per list. |
contactSales | boolean | Gets "Contact sales" instead of an upgrade action. |
Helpers
| Export | Type | Description |
|---|---|---|
ANNUAL_DISCOUNT | number | 0.2. The "Save 20%" badge reads from it. |
annualPrice(plan) | number | null | The monthly price with the discount applied, to a whole dollar. |
formatPrice(plan, cycle) | string | $49, Free or Custom. |
formatCadence(plan, cycle) | string | The line under the price. Empty for a free plan. |
paymentMethod | object | The card on file: Visa, 4242, expiring 08/27. |
acceptedCards | readonly string[] | visa, mastercard, amex, paypal. |
Example
vue
<script setup lang="ts">
import { formatCadence, formatPrice, useSubscription } from '@shared/composables/useSubscription'
const { plans, currentPlan, billingCycle, seatPercent, selectPlan } = useSubscription()
</script>
<template>
<div class="space-y-4">
<button type="button" @click="billingCycle = billingCycle === 'monthly' ? 'annually' : 'monthly'">
Billed {{ billingCycle }}
</button>
<div class="grid gap-3 md:grid-cols-4">
<article v-for="plan in plans" :key="plan.id">
<h3>{{ plan.name }}</h3>
<p>{{ formatPrice(plan, billingCycle) }}</p>
<p v-if="formatCadence(plan, billingCycle)">
{{ formatCadence(plan, billingCycle) }}
</p>
<button
type="button"
:disabled="plan.id === currentPlan.id"
@click="selectPlan(plan.id)"
>
{{ plan.contactSales ? 'Contact sales' : 'Choose' }}
</button>
</article>
</div>
<p>{{ seatPercent }}% of seats used</p>
</div>
</template>Notes
- Prices are numbers, not strings, because the annual figure has to be the discount. A "Save 20%" badge beside a hand-written annual price is a claim any reader with a calculator can falsify.
- Every figure is fixed. A usage bar that moves on each reload reads as noise rather than data, and the limits belong to the seeded plan, so the bars agree with the plan named beside them.
- The seeded plan is one tier below the recommended one, deliberately. Seed it on the recommended tier and the first render has every cheaper tier reading "Downgrade", the recommended card disabled, and the word "Upgrade" nowhere on the page.
- The card is
4242, the universal test number. Anyone who recognizes it knows immediately that no billing provider is wired up; anyone who does not simply reads a plausible card. plansis arefrather than a constant because whoever runs the app may be the one selling the plans, so the page can add a tier. Added tiers are demo state and a reload restores the originals.- A free plan gets no cadence line at all. "Free forever" is a promise the demo cannot make, and
Freealready says what the figure needs to. - It is a module-scope singleton, so the profile card and the plans page cannot show different plans.
Related
- Card brand — the payment marks.
- Showcase — the plans page, and every other page the template ships.
- Internationalization — how the plan names and cadence lines translate.