@frigear-nu/subscriptions 0.0.0-stage โ 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -2
- package/dist/module.d.mts +155 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +120 -0
- package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +71 -0
- package/dist/runtime/app/components/SubscriptionPlans.vue +215 -0
- package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +71 -0
- package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +44 -0
- package/dist/runtime/app/components/SubscriptionStatus.vue +138 -0
- package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +44 -0
- package/dist/runtime/app/composables/index.d.ts +4 -0
- package/dist/runtime/app/composables/index.js +2 -0
- package/dist/runtime/app/composables/usePlans.d.ts +23 -0
- package/dist/runtime/app/composables/usePlans.js +10 -0
- package/dist/runtime/app/composables/useSubscription.d.ts +27 -0
- package/dist/runtime/app/composables/useSubscription.js +73 -0
- package/dist/runtime/app/middleware/subscription.d.ts +2 -0
- package/dist/runtime/app/middleware/subscription.js +14 -0
- package/dist/runtime/app/pages/cancel.d.vue.ts +3 -0
- package/dist/runtime/app/pages/cancel.vue +23 -0
- package/dist/runtime/app/pages/cancel.vue.d.ts +3 -0
- package/dist/runtime/app/pages/membership.d.vue.ts +3 -0
- package/dist/runtime/app/pages/membership.vue +18 -0
- package/dist/runtime/app/pages/membership.vue.d.ts +3 -0
- package/dist/runtime/app/pages/success.d.vue.ts +3 -0
- package/dist/runtime/app/pages/success.vue +33 -0
- package/dist/runtime/app/pages/success.vue.d.ts +3 -0
- package/dist/runtime/i18n/lang/da.json +55 -0
- package/dist/runtime/i18n/lang/en.json +55 -0
- package/dist/runtime/menus.d.ts +2 -0
- package/dist/runtime/menus.js +16 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +231 -0
- package/dist/runtime/server/db/schema.sqlite.js +20 -0
- package/dist/runtime/server/routes/subscriptions/checkout.post.d.ts +2 -0
- package/dist/runtime/server/routes/subscriptions/checkout.post.js +125 -0
- package/dist/runtime/server/routes/subscriptions/me.get.d.ts +2 -0
- package/dist/runtime/server/routes/subscriptions/me.get.js +22 -0
- package/dist/runtime/server/routes/subscriptions/plans.get.d.ts +2 -0
- package/dist/runtime/server/routes/subscriptions/plans.get.js +3 -0
- package/dist/runtime/server/routes/subscriptions/portal.post.d.ts +2 -0
- package/dist/runtime/server/routes/subscriptions/portal.post.js +17 -0
- package/dist/runtime/server/stripe/subscription-events.d.ts +2 -0
- package/dist/runtime/server/stripe/subscription-events.js +36 -0
- package/dist/runtime/server/utils/plans.d.ts +6 -0
- package/dist/runtime/server/utils/plans.js +64 -0
- package/dist/runtime/server/utils/subscriptions.d.ts +13 -0
- package/dist/runtime/server/utils/subscriptions.js +16 -0
- package/dist/runtime/utils/index.d.ts +4 -0
- package/dist/runtime/utils/index.js +2 -0
- package/dist/runtime/utils/plans.d.ts +32 -0
- package/dist/runtime/utils/plans.js +33 -0
- package/dist/types.d.mts +3 -0
- package/package.json +88 -3
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @frigear-nu/subscriptions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Recurring Stripe subscriptions for the frigear ecosystem: host-defined plans, checkout, upgrade/downgrade, billing
|
|
4
|
+
portal, entitlements and opt-in account pages. Built on [`@frigear-nu/stripe`](../stripe) (customer link + verified
|
|
5
|
+
webhook) and [`@frigear-nu/users`](../users). One-off payments are out of scope.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ๐ Plans declared in config; live prices resolved from Stripe and cached in NuxtHub KV (~5 minutes).
|
|
10
|
+
- ๐ Checkout for new subscribers; immediate upgrade (prorated) or deferred downgrade, decided by plan `rank`.
|
|
11
|
+
- ๐งพ Stripe billing portal sessions.
|
|
12
|
+
- ๐ช Policy hooks (`subscriptions:checkout:before`, `subscriptions:plans:filter`) for seasons, discounts, trials, ...
|
|
13
|
+
- ๐ Entitlement utils (`hasActiveSubscription`, `getUserSubscription`) and a `subscription` route middleware.
|
|
14
|
+
- ๐งฉ `usePlans` / `useSubscription` composables, `SubscriptionPlans` / `SubscriptionStatus` components with slots and
|
|
15
|
+
optional i18n, and opt-in membership/success/cancel pages (built with [`@nuxt/ui`](https://ui.nuxt.com)).
|
|
16
|
+
- ๐๏ธ Contributes a "Subscription" item to `@frigear-nu/menu` (when installed).
|
|
17
|
+
|
|
18
|
+
## Quick Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx nuxt module add @frigear-nu/subscriptions
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires `@nuxthub/core` with `hub.kv: true` (the build fails otherwise), `@frigear-nu/users` and `@frigear-nu/stripe`
|
|
25
|
+
(set up its environment variables as described in its README).
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
export default defineNuxtConfig({
|
|
31
|
+
modules: [
|
|
32
|
+
'@nuxthub/core',
|
|
33
|
+
'@frigear-nu/users',
|
|
34
|
+
'@frigear-nu/menu', // optional
|
|
35
|
+
'@frigear-nu/stripe',
|
|
36
|
+
'@frigear-nu/subscriptions',
|
|
37
|
+
],
|
|
38
|
+
hub: { db: { dialect: 'sqlite', driver: 'libsql' }, kv: true },
|
|
39
|
+
subscriptions: {
|
|
40
|
+
plans: {
|
|
41
|
+
member: { label: 'Member', rank: 1, prices: ['price_123', { lookupKey: 'member_yearly' }] },
|
|
42
|
+
patron: { label: 'Patron', description: 'Supporters', rank: 2, prices: [{ lookupKey: 'patron_yearly' }] },
|
|
43
|
+
},
|
|
44
|
+
settings: {
|
|
45
|
+
pages: {
|
|
46
|
+
membership: true, // or a custom path, default '/account/membership'
|
|
47
|
+
success: true, // default '/account/membership/success'
|
|
48
|
+
cancel: true, // default '/account/membership/cancel'
|
|
49
|
+
},
|
|
50
|
+
allowChangeInPortal: false, // default; a hint exposed in public runtime config, not enforced
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `plans` (default `{}`): keyed by plan key. `prices` are Stripe price ids or `{ lookupKey }`; `rank` decides upgrade vs.
|
|
57
|
+
downgrade (never the price amount); `label` defaults to the key. Leave `plans` empty for "any active subscription"
|
|
58
|
+
(checkout then requires a `priceId`).
|
|
59
|
+
- Pages are off unless enabled; `true` uses the default path, a string overrides it.
|
|
60
|
+
|
|
61
|
+
## Database
|
|
62
|
+
|
|
63
|
+
Registers a `subscriptions` table (one row per Stripe subscription: `id`, `userId`, `planKey`, `priceId`, `status`,
|
|
64
|
+
`cancelAtPeriodEnd`, period dates, `endedAt`, `rawItems`, timestamps). Rows are never deleted; prices outside the config are
|
|
65
|
+
stored with `planKey = null`. There is no foreign key to `users`.
|
|
66
|
+
|
|
67
|
+
**Host apps must generate and commit their own migration** (the `subscriptions` table plus `users.stripe_customer_id`
|
|
68
|
+
from `@frigear-nu/stripe`), using the app's complete module configuration. See `AGENTS.md` and the Module Kit docs.
|
|
69
|
+
|
|
70
|
+
## Webhook events
|
|
71
|
+
|
|
72
|
+
Subscribe the Stripe webhook endpoint (see `@frigear-nu/stripe`) to `customer.subscription.created`, `.updated`,
|
|
73
|
+
`.deleted`, `.paused` and `.resumed`. Each event is an idempotent upsert by subscription id; events for customers without
|
|
74
|
+
a local user are logged and ignored.
|
|
75
|
+
|
|
76
|
+
## API routes
|
|
77
|
+
|
|
78
|
+
- `GET /api/subscriptions/plans` - public; `{ plans }` with live prices, after the `subscriptions:plans:filter` hook.
|
|
79
|
+
- `GET /api/subscriptions/me` - `{ subscriptions, activePlan }` (401 when signed out).
|
|
80
|
+
- `POST /api/subscriptions/checkout` - body `{ planKey?, priceId?, successUrl?, cancelUrl? }` (one of `planKey`/`priceId`
|
|
81
|
+
required; a multi-price plan needs `priceId`). Returns `{ url }` (new Checkout Session) or
|
|
82
|
+
`{ updated: true, kind: 'upgrade' | 'downgrade' }`. A higher rank upgrades immediately (`always_invoice` proration);
|
|
83
|
+
a lower or equal rank (including monthly to yearly of one plan) is scheduled for period end. `400` for unknown or hidden
|
|
84
|
+
plans/prices and URLs outside the request origin, `409` when already on that price.
|
|
85
|
+
- `POST /api/subscriptions/portal` - body `{ returnUrl? }`; returns `{ url }` of a billing portal session.
|
|
86
|
+
|
|
87
|
+
## Policy hooks
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// server/plugins/subscription-policy.ts
|
|
91
|
+
export default defineNitroPlugin((nitro) => {
|
|
92
|
+
// ctx: { event, userId, planKey, priceId, kind: 'checkout' | 'upgrade' | 'downgrade', params }
|
|
93
|
+
// Throw to deny. `params` (Stripe Checkout Session params) only applies when `kind === 'checkout'`.
|
|
94
|
+
nitro.hooks.hook('subscriptions:checkout:before', (ctx) => {
|
|
95
|
+
if (ctx.kind === 'checkout') ctx.params.subscription_data = { ...ctx.params.subscription_data, trial_period_days: 7 }
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// ctx: { event (undefined outside a request), plans }. Change `ctx.plans` to hide or reorder plans.
|
|
99
|
+
nitro.hooks.hook('subscriptions:plans:filter', (ctx) => {
|
|
100
|
+
ctx.plans = ctx.plans.filter(plan => plan.key !== 'secret')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Entitlements and middleware
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// server (auto-imported, or from '@frigear-nu/subscriptions/utils')
|
|
109
|
+
const subscription = await getUserSubscription(userId)
|
|
110
|
+
const ok = await hasActiveSubscription(userId, { plans: ['member'], statuses: ['active', 'trialing'] }) // statuses default
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// page
|
|
115
|
+
definePageMeta({ middleware: 'subscription', subscription: { plans: ['member'], redirectTo: '/join' } })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Redirects to `redirectTo` (else the membership page) with `?redirect=<route>`; a no-op when neither exists.
|
|
119
|
+
|
|
120
|
+
## Composables and components
|
|
121
|
+
|
|
122
|
+
- `usePlans()` - `{ plans, status, error, refresh }`.
|
|
123
|
+
- `useSubscription()` - `{ subscription, plan, status, isActive, cancelsAt, has(plan), pending, error, refresh, subscribe(planKeyOrPriceId, { successUrl?, cancelUrl? }), openPortal(returnUrl?) }`.
|
|
124
|
+
- `<SubscriptionPlans>` - props `locale`, `currency`, `interval`, `plans`, `confirmChange`; slots `loading`,
|
|
125
|
+
`empty`, `header`, `badge`, `price`, `features`, `action`, `confirm`, `confirm-actions`.
|
|
126
|
+
- `<SubscriptionStatus>` - props `locale`; slots `header`, `loading`, `empty`, `plan`, `badge`, `dates`, `actions`.
|
|
127
|
+
|
|
128
|
+
Text uses `useModuleT('subscriptionsโฆ')`: `subscriptions.<group>.<key>` from `@nuxtjs/i18n` when defined, else English defaults. Plan names come from `subscriptions.catalog.<planKey>.label` / `.description`, falling back to the plan config.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import Stripe from 'stripe';
|
|
3
|
+
import { H3Event } from 'h3';
|
|
4
|
+
|
|
5
|
+
/** A price of a plan: either a Stripe price id, or a lookup key resolved through Stripe. */
|
|
6
|
+
type PlanPriceRef = string | {
|
|
7
|
+
lookupKey: string;
|
|
8
|
+
};
|
|
9
|
+
type PlanConfig = {
|
|
10
|
+
/** Stripe prices belonging to this plan (e.g. monthly + yearly). */
|
|
11
|
+
prices: PlanPriceRef[];
|
|
12
|
+
/** Display label. Plain text, used as the fallback. The UI prefers `subscriptions.catalog.<planKey>.label` from i18n when defined. */
|
|
13
|
+
label?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Drives upgrade vs. downgrade: a higher rank is a higher tier. */
|
|
16
|
+
rank: number;
|
|
17
|
+
};
|
|
18
|
+
type ModuleOptions = {
|
|
19
|
+
/** Plans keyed by plan key. Leave empty to use the module as "any active subscription". */
|
|
20
|
+
plans?: Record<string, PlanConfig>;
|
|
21
|
+
settings?: {
|
|
22
|
+
pages?: {
|
|
23
|
+
/** Default `/account/membership`. */
|
|
24
|
+
membership?: string | boolean;
|
|
25
|
+
/** Default `/account/membership/success`. */
|
|
26
|
+
success?: string | boolean;
|
|
27
|
+
/** Default `/account/membership/cancel`. */
|
|
28
|
+
cancel?: string | boolean;
|
|
29
|
+
};
|
|
30
|
+
/** Hint for the UI: let customers change plan in the Stripe billing portal instead of in-app. */
|
|
31
|
+
allowChangeInPortal?: boolean;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
type PublicPlan = {
|
|
35
|
+
key: string;
|
|
36
|
+
label: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
rank: number;
|
|
39
|
+
};
|
|
40
|
+
/** Exposed as `useRuntimeConfig().public.subscriptions`. */
|
|
41
|
+
type PublicModuleOptions = {
|
|
42
|
+
pages: {
|
|
43
|
+
membership?: string;
|
|
44
|
+
success?: string;
|
|
45
|
+
cancel?: string;
|
|
46
|
+
};
|
|
47
|
+
plans: PublicPlan[];
|
|
48
|
+
allowChangeInPortal: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Private server runtime config (`useRuntimeConfig().subscriptions`). */
|
|
51
|
+
type SubscriptionsRuntimeConfig = {
|
|
52
|
+
plans: Record<string, {
|
|
53
|
+
prices: PlanPriceRef[];
|
|
54
|
+
label?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
rank: number;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
type PlanPrice = {
|
|
60
|
+
id: string;
|
|
61
|
+
amount: number | null;
|
|
62
|
+
currency: string;
|
|
63
|
+
interval: 'day' | 'week' | 'month' | 'year';
|
|
64
|
+
intervalCount: number;
|
|
65
|
+
trialPeriodDays: number | null;
|
|
66
|
+
lookupKey: string | null;
|
|
67
|
+
};
|
|
68
|
+
type ResolvedPlan = PublicPlan & {
|
|
69
|
+
prices: PlanPrice[];
|
|
70
|
+
};
|
|
71
|
+
type PlansResponse = {
|
|
72
|
+
plans: ResolvedPlan[];
|
|
73
|
+
};
|
|
74
|
+
type SubscriptionDto = {
|
|
75
|
+
id: string;
|
|
76
|
+
planKey: string | null;
|
|
77
|
+
priceId: string;
|
|
78
|
+
status: string;
|
|
79
|
+
cancelAtPeriodEnd: boolean;
|
|
80
|
+
currentPeriodStart: string | null;
|
|
81
|
+
currentPeriodEnd: string | null;
|
|
82
|
+
endedAt: string | null;
|
|
83
|
+
};
|
|
84
|
+
type MeResponse = {
|
|
85
|
+
subscriptions: SubscriptionDto[];
|
|
86
|
+
activePlan: string | null;
|
|
87
|
+
};
|
|
88
|
+
type CheckoutBody = {
|
|
89
|
+
planKey?: string;
|
|
90
|
+
priceId?: string;
|
|
91
|
+
successUrl?: string;
|
|
92
|
+
cancelUrl?: string;
|
|
93
|
+
};
|
|
94
|
+
type CheckoutResponse = {
|
|
95
|
+
url: string;
|
|
96
|
+
} | {
|
|
97
|
+
updated: true;
|
|
98
|
+
kind: 'upgrade' | 'downgrade';
|
|
99
|
+
};
|
|
100
|
+
type PortalBody = {
|
|
101
|
+
returnUrl?: string;
|
|
102
|
+
};
|
|
103
|
+
/** Fired before a checkout / plan change. Throw (e.g. `createError`) to deny; mutate `params` to adjust. */
|
|
104
|
+
type CheckoutBeforeContext = {
|
|
105
|
+
event: H3Event;
|
|
106
|
+
userId: number;
|
|
107
|
+
/** The plan the user is moving to, or `null` when the price isn't part of a configured plan. */
|
|
108
|
+
planKey: string | null;
|
|
109
|
+
priceId: string;
|
|
110
|
+
/** `checkout` = new subscription, otherwise a change of an existing one. */
|
|
111
|
+
kind: 'checkout' | 'upgrade' | 'downgrade';
|
|
112
|
+
/**
|
|
113
|
+
* Parameters for `stripe.checkout.sessions.create` (only used when `kind === 'checkout'`). Adjust
|
|
114
|
+
* them in place to add promo codes, tax, trials, metadata, ...
|
|
115
|
+
*/
|
|
116
|
+
params: Stripe.Checkout.SessionCreateParams;
|
|
117
|
+
};
|
|
118
|
+
type PlansFilterContext = {
|
|
119
|
+
/** The request, or `undefined` when computed outside a request. */
|
|
120
|
+
event: H3Event | undefined;
|
|
121
|
+
/** The plans about to be returned. Mutate or reassign `plans` to hide / reorder them. */
|
|
122
|
+
plans: ResolvedPlan[];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
declare module '@nuxt/schema' {
|
|
126
|
+
interface NuxtConfig {
|
|
127
|
+
subscriptions?: ModuleOptions;
|
|
128
|
+
}
|
|
129
|
+
interface RuntimeConfig {
|
|
130
|
+
subscriptions: SubscriptionsRuntimeConfig;
|
|
131
|
+
}
|
|
132
|
+
interface PublicRuntimeConfig {
|
|
133
|
+
subscriptions: PublicModuleOptions;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
declare module '#app' {
|
|
137
|
+
interface PageMeta {
|
|
138
|
+
subscription?: {
|
|
139
|
+
plans?: string[];
|
|
140
|
+
redirectTo?: string;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
declare module 'nitropack' {
|
|
145
|
+
interface NitroRuntimeHooks {
|
|
146
|
+
/** Policy hook: throw to deny a checkout / plan change, or adjust `ctx.params`. */
|
|
147
|
+
'subscriptions:checkout:before': (ctx: CheckoutBeforeContext) => void | Promise<void>;
|
|
148
|
+
/** Policy hook: hide or reorder plans by changing `ctx.plans` in place. */
|
|
149
|
+
'subscriptions:plans:filter': (ctx: PlansFilterContext) => void | Promise<void>;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
153
|
+
|
|
154
|
+
export { _default as default };
|
|
155
|
+
export type { CheckoutBeforeContext, CheckoutBody, CheckoutResponse, MeResponse, ModuleOptions, PlanConfig, PlanPrice, PlanPriceRef, PlansFilterContext, PlansResponse, PortalBody, PublicModuleOptions, PublicPlan, ResolvedPlan, SubscriptionDto, SubscriptionsRuntimeConfig };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addServerImportsDir, addImports, addComponentsDir, addRouteMiddleware, extendPages } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerLocales, registerDatabaseTable, moduleApiUrl, defineModulePage } from '@frigear-nu/module-kit/utils';
|
|
3
|
+
import { registerStripeEventHandler } from '@frigear-nu/stripe/utils';
|
|
4
|
+
|
|
5
|
+
const MODULE_NAME = "@frigear-nu/subscriptions";
|
|
6
|
+
function resolvePagePath(setting, defaultPath) {
|
|
7
|
+
if (typeof setting === "string") return setting;
|
|
8
|
+
if (setting === true) return defaultPath;
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
const module$1 = defineNuxtModule({
|
|
12
|
+
meta: { name: MODULE_NAME, configKey: "subscriptions" },
|
|
13
|
+
defaults: {
|
|
14
|
+
plans: {},
|
|
15
|
+
settings: { pages: {}, allowChangeInPortal: false }
|
|
16
|
+
},
|
|
17
|
+
moduleDependencies: {
|
|
18
|
+
"@nuxthub/core": {
|
|
19
|
+
version: ">=0.10",
|
|
20
|
+
defaults: { kv: true }
|
|
21
|
+
},
|
|
22
|
+
"@frigear-nu/module-kit": { version: ">=0.0.1" },
|
|
23
|
+
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
24
|
+
"@frigear-nu/stripe": { version: ">=0.0.1" },
|
|
25
|
+
"@frigear-nu/menu": { version: ">=0.0.1", optional: true }
|
|
26
|
+
},
|
|
27
|
+
async setup(options, nuxt) {
|
|
28
|
+
registerModule(MODULE_NAME, nuxt);
|
|
29
|
+
const resolver = createResolver(import.meta.url);
|
|
30
|
+
registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
|
|
31
|
+
const settings = options.settings ?? {};
|
|
32
|
+
const plans = options.plans ?? {};
|
|
33
|
+
const withMenus = hasNuxtModule("@frigear-nu/menu");
|
|
34
|
+
if (!nuxt.options.hub?.kv) {
|
|
35
|
+
throw new Error("[@frigear-nu/subscriptions] NuxtHub KV is required (plans cache). Set `hub: { kv: true }` in your Nuxt config.");
|
|
36
|
+
}
|
|
37
|
+
const membershipPath = resolvePagePath(settings.pages?.membership, "/account/membership");
|
|
38
|
+
const successPath = resolvePagePath(settings.pages?.success, "/account/membership/success");
|
|
39
|
+
const cancelPath = resolvePagePath(settings.pages?.cancel, "/account/membership/cancel");
|
|
40
|
+
const planEntries = Object.entries(plans);
|
|
41
|
+
nuxt.options.runtimeConfig.subscriptions = {
|
|
42
|
+
plans: Object.fromEntries(planEntries.map(([key, plan]) => [key, {
|
|
43
|
+
prices: plan.prices,
|
|
44
|
+
label: plan.label,
|
|
45
|
+
description: plan.description,
|
|
46
|
+
rank: plan.rank
|
|
47
|
+
}]))
|
|
48
|
+
};
|
|
49
|
+
nuxt.options.runtimeConfig.public.subscriptions = {
|
|
50
|
+
pages: { membership: membershipPath, success: successPath, cancel: cancelPath },
|
|
51
|
+
plans: planEntries.map(([key, plan]) => ({ key, label: plan.label ?? key, description: plan.description, rank: plan.rank })).sort((a, b) => a.rank - b.rank),
|
|
52
|
+
allowChangeInPortal: settings.allowChangeInPortal === true
|
|
53
|
+
};
|
|
54
|
+
await registerDatabaseTable(nuxt, {
|
|
55
|
+
name: "subscriptions",
|
|
56
|
+
resolver,
|
|
57
|
+
schema: (dialect) => `./runtime/server/db/schema.${dialect}`
|
|
58
|
+
});
|
|
59
|
+
registerStripeEventHandler(nuxt, {
|
|
60
|
+
events: [
|
|
61
|
+
"customer.subscription.created",
|
|
62
|
+
"customer.subscription.updated",
|
|
63
|
+
"customer.subscription.deleted",
|
|
64
|
+
"customer.subscription.paused",
|
|
65
|
+
"customer.subscription.resumed"
|
|
66
|
+
],
|
|
67
|
+
handler: resolver.resolve("./runtime/server/stripe/subscription-events")
|
|
68
|
+
});
|
|
69
|
+
addServerHandler({
|
|
70
|
+
route: moduleApiUrl("subscriptions", "plans"),
|
|
71
|
+
method: "get",
|
|
72
|
+
handler: resolver.resolve("./runtime/server/routes/subscriptions/plans.get")
|
|
73
|
+
});
|
|
74
|
+
addServerHandler({
|
|
75
|
+
route: moduleApiUrl("subscriptions", "me"),
|
|
76
|
+
method: "get",
|
|
77
|
+
handler: resolver.resolve("./runtime/server/routes/subscriptions/me.get")
|
|
78
|
+
});
|
|
79
|
+
addServerHandler({
|
|
80
|
+
route: moduleApiUrl("subscriptions", "checkout"),
|
|
81
|
+
method: "post",
|
|
82
|
+
handler: resolver.resolve("./runtime/server/routes/subscriptions/checkout.post")
|
|
83
|
+
});
|
|
84
|
+
addServerHandler({
|
|
85
|
+
route: moduleApiUrl("subscriptions", "portal"),
|
|
86
|
+
method: "post",
|
|
87
|
+
handler: resolver.resolve("./runtime/server/routes/subscriptions/portal.post")
|
|
88
|
+
});
|
|
89
|
+
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
90
|
+
addImports([
|
|
91
|
+
{ name: "usePlans", from: resolver.resolve("./runtime/app/composables/usePlans") },
|
|
92
|
+
{ name: "useSubscription", from: resolver.resolve("./runtime/app/composables/useSubscription") }
|
|
93
|
+
]);
|
|
94
|
+
addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
|
|
95
|
+
addRouteMiddleware({
|
|
96
|
+
name: "subscription",
|
|
97
|
+
path: resolver.resolve("./runtime/app/middleware/subscription")
|
|
98
|
+
});
|
|
99
|
+
if (membershipPath || successPath || cancelPath) {
|
|
100
|
+
extendPages((pages) => {
|
|
101
|
+
const existingPaths = new Set(pages.map((page) => page.path));
|
|
102
|
+
if (membershipPath && !existingPaths.has(membershipPath)) {
|
|
103
|
+
pages.push(defineModulePage(MODULE_NAME, { name: "subscriptions-membership", path: membershipPath, file: resolver.resolve("./runtime/app/pages/membership.vue") }));
|
|
104
|
+
}
|
|
105
|
+
if (successPath && !existingPaths.has(successPath)) {
|
|
106
|
+
pages.push(defineModulePage(MODULE_NAME, { name: "subscriptions-success", path: successPath, file: resolver.resolve("./runtime/app/pages/success.vue") }));
|
|
107
|
+
}
|
|
108
|
+
if (cancelPath && !existingPaths.has(cancelPath)) {
|
|
109
|
+
pages.push(defineModulePage(MODULE_NAME, { name: "subscriptions-cancel", path: cancelPath, file: resolver.resolve("./runtime/app/pages/cancel.vue") }));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (withMenus) {
|
|
114
|
+
const { registerMenuSource } = await import('@frigear-nu/menu/utils');
|
|
115
|
+
registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SubscriptionPlan, SubscriptionPlanPrice } from '../composables/usePlans.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
locale?: string;
|
|
4
|
+
/** Override the price currency formatting currency (defaults to the price's own currency). */
|
|
5
|
+
currency?: string;
|
|
6
|
+
interval?: SubscriptionPlanPrice['interval'];
|
|
7
|
+
/** Only show these plan keys. */
|
|
8
|
+
plans?: string[];
|
|
9
|
+
/** Ask for confirmation (modal) before changing an existing subscription. */
|
|
10
|
+
confirmChange?: boolean;
|
|
11
|
+
};
|
|
12
|
+
declare function formatPrice(price: SubscriptionPlanPrice): string;
|
|
13
|
+
declare function intervalLabel(price: SubscriptionPlanPrice): any;
|
|
14
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_17: {
|
|
15
|
+
plan: any;
|
|
16
|
+
state: "upgrade" | "downgrade" | "subscribe" | "current";
|
|
17
|
+
}, __VLS_19: {
|
|
18
|
+
plan: any;
|
|
19
|
+
state: "upgrade" | "downgrade" | "subscribe" | "current";
|
|
20
|
+
}, __VLS_27: {
|
|
21
|
+
plan: any;
|
|
22
|
+
price: SubscriptionPlanPrice | undefined;
|
|
23
|
+
formatPrice: typeof formatPrice;
|
|
24
|
+
intervalLabel: typeof intervalLabel;
|
|
25
|
+
}, __VLS_29: {
|
|
26
|
+
plan: any;
|
|
27
|
+
}, __VLS_32: {
|
|
28
|
+
plan: any;
|
|
29
|
+
state: "upgrade" | "downgrade" | "subscribe" | "current";
|
|
30
|
+
pending: any;
|
|
31
|
+
select: () => void;
|
|
32
|
+
}, __VLS_48: {
|
|
33
|
+
plan: any;
|
|
34
|
+
price: any;
|
|
35
|
+
}, __VLS_51: {
|
|
36
|
+
plan: any;
|
|
37
|
+
price: any;
|
|
38
|
+
confirm: () => any;
|
|
39
|
+
};
|
|
40
|
+
type __VLS_Slots = {} & {
|
|
41
|
+
loading?: (props: typeof __VLS_1) => any;
|
|
42
|
+
} & {
|
|
43
|
+
empty?: (props: typeof __VLS_3) => any;
|
|
44
|
+
} & {
|
|
45
|
+
header?: (props: typeof __VLS_17) => any;
|
|
46
|
+
} & {
|
|
47
|
+
badge?: (props: typeof __VLS_19) => any;
|
|
48
|
+
} & {
|
|
49
|
+
price?: (props: typeof __VLS_27) => any;
|
|
50
|
+
} & {
|
|
51
|
+
features?: (props: typeof __VLS_29) => any;
|
|
52
|
+
} & {
|
|
53
|
+
action?: (props: typeof __VLS_32) => any;
|
|
54
|
+
} & {
|
|
55
|
+
confirm?: (props: typeof __VLS_48) => any;
|
|
56
|
+
} & {
|
|
57
|
+
'confirm-actions'?: (props: typeof __VLS_51) => any;
|
|
58
|
+
};
|
|
59
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
60
|
+
select: (plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any;
|
|
61
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
62
|
+
onSelect?: ((plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any) | undefined;
|
|
63
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
64
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
65
|
+
declare const _default: typeof __VLS_export;
|
|
66
|
+
export default _default;
|
|
67
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
68
|
+
new (): {
|
|
69
|
+
$slots: S;
|
|
70
|
+
};
|
|
71
|
+
};
|