@frigear-nu/subscriptions 0.0.6 → 0.0.8
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/dist/module.d.mts +65 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +60 -29
- package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +31 -27
- package/dist/runtime/app/components/SubscriptionPlans.vue +203 -105
- package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +31 -27
- package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +1 -1
- package/dist/runtime/app/components/SubscriptionStatus.vue +45 -24
- package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +1 -1
- package/dist/runtime/app/components/Users/Admin/Membership.d.vue.ts +14 -0
- package/dist/runtime/app/components/Users/Admin/Membership.vue +192 -0
- package/dist/runtime/app/components/Users/Admin/Membership.vue.d.ts +14 -0
- package/dist/runtime/app/composables/usePlans.d.ts +11 -0
- package/dist/runtime/app/composables/usePlans.js +9 -1
- package/dist/runtime/app/composables/useSubscription.d.ts +1 -0
- package/dist/runtime/app/composables/useSubscription.js +17 -3
- package/dist/runtime/app/pages/cancel.d.vue.ts +1 -1
- package/dist/runtime/app/pages/cancel.vue.d.ts +1 -1
- package/dist/runtime/app/pages/membership.d.vue.ts +1 -1
- package/dist/runtime/app/pages/membership.vue +18 -1
- package/dist/runtime/app/pages/membership.vue.d.ts +1 -1
- package/dist/runtime/app/pages/success.d.vue.ts +1 -1
- package/dist/runtime/app/pages/success.vue +9 -3
- package/dist/runtime/app/pages/success.vue.d.ts +1 -1
- package/dist/runtime/app/plugins/warm-plans.client.d.ts +6 -0
- package/dist/runtime/app/plugins/warm-plans.client.js +12 -0
- package/dist/runtime/app/tabs/Membership.d.vue.ts +14 -0
- package/dist/runtime/app/tabs/Membership.vue +53 -0
- package/dist/runtime/app/tabs/Membership.vue.d.ts +14 -0
- package/dist/runtime/app/widgets/AdminCurrentSubscription.d.vue.ts +11 -0
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue +23 -0
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue.d.ts +11 -0
- package/dist/runtime/i18n/lang/da.json +67 -3
- package/dist/runtime/i18n/lang/en.json +67 -3
- package/dist/runtime/menus.js +2 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +19 -0
- package/dist/runtime/server/db/schema.sqlite.js +2 -0
- package/dist/runtime/server/mcp/tools/my-subscriptions.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/my-subscriptions.js +9 -0
- package/dist/runtime/server/routes/admin/payment-link.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/payment-link.post.js +46 -0
- package/dist/runtime/server/routes/admin/subscription.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscription.delete.js +14 -0
- package/dist/runtime/server/routes/admin/subscription.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscription.get.js +17 -0
- package/dist/runtime/server/routes/admin/subscription.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscription.post.js +44 -0
- package/dist/runtime/server/routes/admin/subscriptions.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscriptions.get.js +4 -0
- package/dist/runtime/server/routes/subscriptions/checkout.post.js +16 -30
- package/dist/runtime/server/routes/subscriptions/me.get.js +2 -20
- package/dist/runtime/server/routes/subscriptions/portal.post.js +2 -2
- package/dist/runtime/server/stripe/invoice-events.d.ts +2 -0
- package/dist/runtime/server/stripe/invoice-events.js +42 -0
- package/dist/runtime/server/utils/admin-membership.d.ts +5 -0
- package/dist/runtime/server/utils/admin-membership.js +7 -0
- package/dist/runtime/server/utils/checkout.d.ts +15 -0
- package/dist/runtime/server/utils/checkout.js +22 -0
- package/dist/runtime/server/utils/plans.js +20 -1
- package/dist/runtime/server/utils/subscriptions.d.ts +3 -0
- package/dist/runtime/server/utils/subscriptions.js +22 -3
- package/dist/runtime/utils/index.d.ts +1 -1
- package/dist/runtime/utils/index.js +1 -1
- package/dist/runtime/utils/plans.d.ts +15 -6
- package/dist/runtime/utils/plans.js +16 -11
- package/dist/types.d.mts +1 -1
- package/package.json +18 -5
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { kv } from "@nuxthub/kv";
|
|
2
|
+
import { isWithinDisabledRanges } from "../../utils/plans.js";
|
|
3
|
+
import { getUserSubscription } from "./subscriptions.js";
|
|
2
4
|
const CACHE_TTL_SECONDS = 60 * 5;
|
|
3
5
|
const INTERVALS = ["day", "week", "month", "year"];
|
|
4
6
|
function toPlanPrice(price) {
|
|
@@ -49,6 +51,7 @@ export async function getResolvedPlans() {
|
|
|
49
51
|
key,
|
|
50
52
|
label: plan.label ?? key,
|
|
51
53
|
...plan.description ? { description: plan.description } : {},
|
|
54
|
+
...plan.features?.length ? { features: plan.features } : {},
|
|
52
55
|
rank: plan.rank,
|
|
53
56
|
prices: resolved
|
|
54
57
|
};
|
|
@@ -58,7 +61,23 @@ export async function getResolvedPlans() {
|
|
|
58
61
|
return plans;
|
|
59
62
|
}
|
|
60
63
|
export async function getAvailablePlans(event) {
|
|
61
|
-
const
|
|
64
|
+
const config = useRuntimeConfig().subscriptions?.plans ?? {};
|
|
65
|
+
let plans = structuredClone(await getResolvedPlans());
|
|
66
|
+
if (Object.values(config).some((plan) => plan.availability?.disabledRanges?.length)) {
|
|
67
|
+
const subscriber = await isSubscriber(event);
|
|
68
|
+
if (!subscriber) {
|
|
69
|
+
const now = /* @__PURE__ */ new Date();
|
|
70
|
+
plans = plans.filter((plan) => !isWithinDisabledRanges(config[plan.key]?.availability?.disabledRanges, now));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const ctx = { event, plans };
|
|
62
74
|
await useNitroApp().hooks.callHook("subscriptions:plans:filter", ctx);
|
|
63
75
|
return ctx.plans;
|
|
64
76
|
}
|
|
77
|
+
async function isSubscriber(event) {
|
|
78
|
+
if (!event) return false;
|
|
79
|
+
const user = await getUser(event, { scopes: ["subscriptions:read"] }).catch(() => null);
|
|
80
|
+
const userId = user?.id;
|
|
81
|
+
if (userId === void 0 || userId === null) return false;
|
|
82
|
+
return !!await getUserSubscription(Number(userId));
|
|
83
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { MeResponse } from '../../../types.js';
|
|
1
2
|
export type HasActiveSubscriptionOptions = {
|
|
2
3
|
/** Only count subscriptions on one of these plan keys. Omit for "any plan". */
|
|
3
4
|
plans?: string[];
|
|
4
5
|
/** Statuses that count as entitled. Defaults to `active` and `trialing`. */
|
|
5
6
|
statuses?: string[];
|
|
6
7
|
};
|
|
8
|
+
/** Every subscription row of the user, newest first, in the shape the API and MCP expose. */
|
|
9
|
+
export declare function listUserSubscriptions(userId: number): Promise<MeResponse>;
|
|
7
10
|
/**
|
|
8
11
|
* The user's current entitled subscription row (default statuses `active`/`trialing`, not ended), the one
|
|
9
12
|
* with the latest period end when several match, or `null`.
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import { db, schema } from "@nuxthub/db";
|
|
2
2
|
import { desc, eq } from "drizzle-orm";
|
|
3
|
-
import { DEFAULT_ENTITLED_STATUSES } from "../../utils/plans.js";
|
|
3
|
+
import { DEFAULT_ENTITLED_STATUSES, isEntitledRow } from "../../utils/plans.js";
|
|
4
|
+
export async function listUserSubscriptions(userId) {
|
|
5
|
+
const rows = await db.select().from(schema.subscriptions).where(eq(schema.subscriptions.userId, userId)).orderBy(desc(schema.subscriptions.createdAt));
|
|
6
|
+
const iso = (date) => date ? date.toISOString() : null;
|
|
7
|
+
const active = rows.filter((row) => isEntitledRow(row)).sort((a, b) => (b.currentPeriodEnd?.getTime() ?? 0) - (a.currentPeriodEnd?.getTime() ?? 0))[0];
|
|
8
|
+
return {
|
|
9
|
+
subscriptions: rows.map((row) => ({
|
|
10
|
+
id: row.id,
|
|
11
|
+
planKey: row.planKey,
|
|
12
|
+
priceId: row.priceId,
|
|
13
|
+
status: row.status,
|
|
14
|
+
cancelAtPeriodEnd: row.cancelAtPeriodEnd,
|
|
15
|
+
currentPeriodStart: iso(row.currentPeriodStart),
|
|
16
|
+
currentPeriodEnd: iso(row.currentPeriodEnd),
|
|
17
|
+
endedAt: iso(row.endedAt),
|
|
18
|
+
source: row.source === "manual" ? "manual" : "stripe"
|
|
19
|
+
})),
|
|
20
|
+
activePlan: active?.planKey ?? null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
4
23
|
export async function getUserSubscription(userId, options = {}) {
|
|
5
24
|
const statuses = options.statuses ?? DEFAULT_ENTITLED_STATUSES;
|
|
6
25
|
const rows = await db.select().from(schema.subscriptions).where(eq(schema.subscriptions.userId, userId)).orderBy(desc(schema.subscriptions.createdAt));
|
|
7
|
-
const entitled = rows.filter((row) =>
|
|
26
|
+
const entitled = rows.filter((row) => isEntitledRow(row, statuses)).sort((a, b) => (b.currentPeriodEnd?.getTime() ?? 0) - (a.currentPeriodEnd?.getTime() ?? 0));
|
|
8
27
|
return entitled[0] ?? null;
|
|
9
28
|
}
|
|
10
29
|
export async function hasActiveSubscription(userId, options = {}) {
|
|
11
30
|
const statuses = options.statuses ?? DEFAULT_ENTITLED_STATUSES;
|
|
12
31
|
const rows = await db.select().from(schema.subscriptions).where(eq(schema.subscriptions.userId, userId));
|
|
13
32
|
return rows.some(
|
|
14
|
-
(row) =>
|
|
33
|
+
(row) => isEntitledRow(row, statuses) && (!options.plans?.length || row.planKey !== null && options.plans.includes(row.planKey))
|
|
15
34
|
);
|
|
16
35
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { getUserSubscription, hasActiveSubscription } from '../server/utils/subscriptions.js';
|
|
2
2
|
export type { HasActiveSubscriptionOptions } from '../server/utils/subscriptions.js';
|
|
3
|
-
export { decideChangeKind, resolvePlanKey, resolveSameOriginUrl, DEFAULT_ENTITLED_STATUSES } from './plans.js';
|
|
3
|
+
export { decideChangeKind, resolvePlanKey, resolveSameOriginUrl, isWithinDisabledRanges, DEFAULT_ENTITLED_STATUSES } from './plans.js';
|
|
4
4
|
export type { CheckoutBeforeContext, PlansFilterContext, ResolvedPlan, PlanPrice, SubscriptionDto, ModuleOptions, } from '../../types.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { getUserSubscription, hasActiveSubscription } from "../server/utils/subscriptions.js";
|
|
2
|
-
export { decideChangeKind, resolvePlanKey, resolveSameOriginUrl, DEFAULT_ENTITLED_STATUSES } from "./plans.js";
|
|
2
|
+
export { decideChangeKind, resolvePlanKey, resolveSameOriginUrl, isWithinDisabledRanges, DEFAULT_ENTITLED_STATUSES } from "./plans.js";
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type { PlanPriceRef, ResolvedPlan } from '../../types.js';
|
|
1
|
+
import type { PlanDisabledRange, PlanPriceRef, ResolvedPlan } from '../../types.js';
|
|
2
2
|
export declare const DEFAULT_ENTITLED_STATUSES: string[];
|
|
3
3
|
/** Statuses that count as "holds a live subscription" when deciding between a new checkout and a plan change. */
|
|
4
|
+
/**
|
|
5
|
+
* Whether a stored row currently entitles its user: an entitled status, not ended, and (for manual grants,
|
|
6
|
+
* which have no Stripe lifecycle to end them) not past its optional end date.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isEntitledRow(row: {
|
|
9
|
+
status: string;
|
|
10
|
+
endedAt: Date | null;
|
|
11
|
+
source?: string | null;
|
|
12
|
+
currentPeriodEnd: Date | null;
|
|
13
|
+
}, statuses?: string[], now?: number): boolean;
|
|
4
14
|
export declare const LIVE_STATUSES: string[];
|
|
5
15
|
type PlanConfigLike = Record<string, {
|
|
6
16
|
prices: PlanPriceRef[];
|
|
@@ -19,14 +29,13 @@ export type ChangeKind = 'upgrade' | 'downgrade';
|
|
|
19
29
|
* plan that isn't configured counts as the lowest rank.
|
|
20
30
|
*/
|
|
21
31
|
export declare function decideChangeKind(currentRank: number | null | undefined, targetRank: number): ChangeKind;
|
|
22
|
-
/**
|
|
23
|
-
* Validates a redirect URL supplied by a client: it must be a relative path (`/x`) or an absolute URL on
|
|
24
|
-
* `origin`. Returns an absolute URL on that origin, or `null` when it is unsafe.
|
|
25
|
-
*/
|
|
26
|
-
export declare function resolveSameOriginUrl(input: string, origin: string): string | null;
|
|
27
32
|
/** Finds a price (and its plan) in already resolved plans by price id. */
|
|
28
33
|
export declare function findPrice(plans: ResolvedPlan[], priceId: string): {
|
|
29
34
|
plan: ResolvedPlan;
|
|
30
35
|
price: import(".").PlanPrice;
|
|
31
36
|
} | null;
|
|
37
|
+
/** Parses a range date; `{fullYear}` becomes the current year. Invalid dates yield `null`. */
|
|
38
|
+
export declare function parseRangeDate(text: string, now?: Date): Date | null;
|
|
39
|
+
/** Whether `now` falls (inclusively) within any of the ranges. Unparseable ranges are ignored. */
|
|
40
|
+
export declare function isWithinDisabledRanges(ranges: PlanDisabledRange[] | undefined, now?: Date): boolean;
|
|
32
41
|
export {};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export const DEFAULT_ENTITLED_STATUSES = ["active", "trialing"];
|
|
2
|
+
export function isEntitledRow(row, statuses = DEFAULT_ENTITLED_STATUSES, now = Date.now()) {
|
|
3
|
+
if (!statuses.includes(row.status) || row.endedAt) return false;
|
|
4
|
+
if (row.source === "manual" && row.currentPeriodEnd && row.currentPeriodEnd.getTime() <= now) return false;
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
2
7
|
export const LIVE_STATUSES = ["active", "trialing", "past_due"];
|
|
3
8
|
export function resolvePlanKey(plans, price) {
|
|
4
9
|
for (const [key, plan] of Object.entries(plans)) {
|
|
@@ -13,17 +18,6 @@ export function decideChangeKind(currentRank, targetRank) {
|
|
|
13
18
|
if (currentRank === null || currentRank === void 0) return "upgrade";
|
|
14
19
|
return targetRank > currentRank ? "upgrade" : "downgrade";
|
|
15
20
|
}
|
|
16
|
-
export function resolveSameOriginUrl(input, origin) {
|
|
17
|
-
if (input.startsWith("/")) {
|
|
18
|
-
if (input.startsWith("//") || input.startsWith("/\\")) return null;
|
|
19
|
-
return `${origin}${input}`;
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
return new URL(input).origin === new URL(origin).origin ? input : null;
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
21
|
export function findPrice(plans, priceId) {
|
|
28
22
|
for (const plan of plans) {
|
|
29
23
|
const price = plan.prices.find((item) => item.id === priceId);
|
|
@@ -31,3 +25,14 @@ export function findPrice(plans, priceId) {
|
|
|
31
25
|
}
|
|
32
26
|
return null;
|
|
33
27
|
}
|
|
28
|
+
export function parseRangeDate(text, now = /* @__PURE__ */ new Date()) {
|
|
29
|
+
const date = new Date(text.replaceAll("{fullYear}", String(now.getFullYear())));
|
|
30
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
31
|
+
}
|
|
32
|
+
export function isWithinDisabledRanges(ranges, now = /* @__PURE__ */ new Date()) {
|
|
33
|
+
return (ranges ?? []).some((range) => {
|
|
34
|
+
const from = parseRangeDate(range.from, now);
|
|
35
|
+
const to = parseRangeDate(range.to, now);
|
|
36
|
+
return !!from && !!to && now >= from && now <= to;
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default } from './module.mjs'
|
|
2
2
|
|
|
3
|
-
export { type CheckoutBeforeContext, type CheckoutBody, type CheckoutResponse, type MeResponse, type ModuleOptions, type PlanConfig, type PlanPrice, type PlanPriceRef, type PlansFilterContext, type PlansResponse, type PortalBody, type PublicModuleOptions, type PublicPlan, type ResolvedPlan, type SubscriptionDto, type SubscriptionsRuntimeConfig } from './module.mjs'
|
|
3
|
+
export { type AdminMembershipResponse, type AdminPaymentLinkResponse, type CheckoutBeforeContext, type CheckoutBody, type CheckoutResponse, type InvoicePaymentEvent, type MeResponse, type ModuleOptions, type PlanAvailability, type PlanConfig, type PlanDisabledRange, type PlanPrice, type PlanPriceRef, type PlansFilterContext, type PlansResponse, type PortalBody, type PublicModuleOptions, type PublicPlan, type ResolvedPlan, type SubscriptionDto, type SubscriptionsRuntimeConfig } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/subscriptions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Recurring subscriptions (plans, checkout, upgrade/downgrade, billing portal and entitlements) for the frigear ecosystem, built on @frigear-nu/stripe.",
|
|
5
|
+
"homepage": "https://docs.frigear.dev/modules/subscriptions",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "git+https://github.com/frigear-nu/frigear-nu.git",
|
|
@@ -29,10 +30,12 @@
|
|
|
29
30
|
"playground"
|
|
30
31
|
],
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@frigear-nu/
|
|
33
|
-
"@frigear-nu/
|
|
34
|
-
"@frigear-nu/
|
|
35
|
-
"@frigear-nu/
|
|
33
|
+
"@frigear-nu/auth": "0.0.3",
|
|
34
|
+
"@frigear-nu/mcp": "0.0.3",
|
|
35
|
+
"@frigear-nu/module-kit": "0.1.7",
|
|
36
|
+
"@frigear-nu/stripe": "0.0.8",
|
|
37
|
+
"@frigear-nu/ui": "0.0.7",
|
|
38
|
+
"@frigear-nu/users": "1.0.9",
|
|
36
39
|
"@libsql/client": "^0.18.0",
|
|
37
40
|
"@nuxt/kit": "^4.6.0",
|
|
38
41
|
"@vueuse/core": "^15.0.0",
|
|
@@ -44,15 +47,25 @@
|
|
|
44
47
|
"zod": "^4.6.5"
|
|
45
48
|
},
|
|
46
49
|
"peerDependencies": {
|
|
50
|
+
"@frigear-nu/menu": "0.0.9",
|
|
51
|
+
"@frigear-nu/widget": "0.0.2",
|
|
47
52
|
"@nuxt/ui": "^4.10.0",
|
|
48
53
|
"@nuxthub/core": "^0.10.8"
|
|
49
54
|
},
|
|
50
55
|
"peerDependenciesMeta": {
|
|
56
|
+
"@frigear-nu/menu": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@frigear-nu/widget": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
51
62
|
"@nuxt/ui": {
|
|
52
63
|
"optional": true
|
|
53
64
|
}
|
|
54
65
|
},
|
|
55
66
|
"devDependencies": {
|
|
67
|
+
"@frigear-nu/menu": "0.0.9",
|
|
68
|
+
"@frigear-nu/widget": "0.0.2",
|
|
56
69
|
"@nuxt/devtools": "^3.4.2",
|
|
57
70
|
"@nuxt/eslint-config": "^1.17.0",
|
|
58
71
|
"@nuxt/module-builder": "^1.0.3",
|