@frigear-nu/subscriptions 0.1.2 → 0.2.0
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 +3 -3
- package/dist/module.d.mts +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -31
- package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +51 -5
- package/dist/runtime/app/components/SubscriptionPlans.vue +6 -11
- package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +51 -5
- package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +16 -6
- package/dist/runtime/app/components/SubscriptionStatus.vue +25 -24
- package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +16 -6
- package/dist/runtime/app/components/Users/Admin/Membership.vue +4 -2
- package/dist/runtime/app/composables/usePlans.js +3 -1
- package/dist/runtime/app/composables/useSubscription.js +5 -3
- package/dist/runtime/app/middleware/subscription.d.ts +1 -1
- package/dist/runtime/app/middleware/subscription.js +1 -0
- package/dist/runtime/app/pages/cancel.vue +19 -6
- package/dist/runtime/app/pages/membership.vue +29 -17
- package/dist/runtime/app/pages/success.vue +19 -6
- package/dist/runtime/app/tabs/Membership.vue +9 -8
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue +1 -0
- package/dist/runtime/app/widgets/MembershipSummary.vue +1 -0
- package/dist/runtime/i18n/lang/da.json +13 -11
- package/dist/runtime/i18n/lang/en.json +15 -13
- package/dist/runtime/menus.js +5 -4
- package/dist/runtime/server/routes/admin/{subscription.delete.js → membership.delete.js} +3 -2
- package/dist/runtime/server/routes/admin/{subscription.get.js → membership.get.js} +2 -2
- package/dist/runtime/server/routes/admin/{subscription.post.js → membership.post.js} +9 -14
- package/dist/runtime/server/routes/admin/payment-link.post.js +3 -2
- package/dist/runtime/server/routes/{subscriptions → me}/checkout.post.js +9 -18
- package/dist/runtime/server/routes/{subscriptions → me}/portal.post.js +5 -8
- package/dist/runtime/server/utils/checkout.js +6 -5
- package/dist/runtime/server/utils/subscriptions.d.ts +6 -1
- package/dist/runtime/server/utils/subscriptions.js +15 -12
- package/dist/runtime/utils/api.d.ts +20 -0
- package/dist/runtime/utils/api.js +23 -0
- package/dist/runtime/utils/index.d.ts +2 -1
- package/dist/runtime/utils/index.js +2 -1
- package/package.json +17 -12
- /package/dist/runtime/server/routes/admin/{subscription.delete.d.ts → membership-history.get.d.ts} +0 -0
- /package/dist/runtime/server/routes/admin/{subscriptions.get.js → membership-history.get.js} +0 -0
- /package/dist/runtime/server/routes/admin/{subscription.get.d.ts → membership.delete.d.ts} +0 -0
- /package/dist/runtime/server/routes/admin/{subscription.post.d.ts → membership.get.d.ts} +0 -0
- /package/dist/runtime/server/routes/admin/{subscriptions.get.d.ts → membership.post.d.ts} +0 -0
- /package/dist/runtime/server/routes/{subscriptions → me}/checkout.post.d.ts +0 -0
- /package/dist/runtime/server/routes/{subscriptions → me}/portal.post.d.ts +0 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ webhook) and [`@frigear-nu/users`](../users). One-off payments are out of scope.
|
|
|
13
13
|
- 🔐 Entitlement utils (`hasActiveSubscription`, `getUserSubscription`) and a `subscription` route middleware.
|
|
14
14
|
- 🧩 `usePlans` / `useSubscription` composables, `SubscriptionPlans` / `SubscriptionStatus` components with slots and
|
|
15
15
|
optional i18n, and opt-in membership/success/cancel pages (built with [`@nuxt/ui`](https://ui.nuxt.com)).
|
|
16
|
-
- 🗂️ Contributes a "
|
|
16
|
+
- 🗂️ Contributes a "Membership" item to `@frigear-nu/menu` (when installed).
|
|
17
17
|
|
|
18
18
|
## Quick Setup
|
|
19
19
|
|
|
@@ -77,12 +77,12 @@ a local user are logged and ignored.
|
|
|
77
77
|
|
|
78
78
|
- `GET /api/subscriptions/plans` - public; `{ plans }` with live prices, after the `subscriptions:plans:filter` hook.
|
|
79
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`
|
|
80
|
+
- `POST /api/subscriptions/me/checkout` - session only (a bearer token gets 403 `session_required`); body `{ planKey?, priceId?, successUrl?, cancelUrl? }` (one of `planKey`/`priceId`
|
|
81
81
|
required; a multi-price plan needs `priceId`). Returns `{ url }` (new Checkout Session) or
|
|
82
82
|
`{ updated: true, kind: 'upgrade' | 'downgrade' }`. A higher rank upgrades immediately (`always_invoice` proration);
|
|
83
83
|
a lower or equal rank (including monthly to yearly of one plan) is scheduled for period end. `400` for unknown or hidden
|
|
84
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.
|
|
85
|
+
- `POST /api/subscriptions/me/portal` - session only; body `{ returnUrl? }`; returns `{ url }` of a billing portal session.
|
|
86
86
|
|
|
87
87
|
## Policy hooks
|
|
88
88
|
|
package/dist/module.d.mts
CHANGED
|
@@ -140,7 +140,7 @@ type PlansFilterContext = {
|
|
|
140
140
|
/** The plans about to be returned. Mutate or reassign `plans` to hide / reorder them. */
|
|
141
141
|
plans: ResolvedPlan[];
|
|
142
142
|
};
|
|
143
|
-
/** `GET /api/admin/users/:id/
|
|
143
|
+
/** `GET /api/admin/users/:id/membership` (staff). */
|
|
144
144
|
type AdminMembershipResponse = {
|
|
145
145
|
plans: Array<{
|
|
146
146
|
key: string;
|
|
@@ -156,7 +156,7 @@ type AdminMembershipResponse = {
|
|
|
156
156
|
cancelAtPeriodEnd: boolean;
|
|
157
157
|
} | null;
|
|
158
158
|
};
|
|
159
|
-
/** `POST /api/admin/users/:id/
|
|
159
|
+
/** `POST /api/admin/users/:id/membership/payment-link` (staff). */
|
|
160
160
|
type AdminPaymentLinkResponse = {
|
|
161
161
|
url: string;
|
|
162
162
|
emailed: boolean;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, hasNuxtModule,
|
|
2
|
-
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable,
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addImports, addComponentsDir, addRouteMiddleware } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, addModuleRoutes, registerBootstrapProvider, inlineModuleRuntime, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
3
3
|
import { addAuthScope } from '@frigear-nu/auth/utils';
|
|
4
4
|
import { extendUsersAdmin } from '@frigear-nu/users/utils';
|
|
5
5
|
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
@@ -37,8 +37,7 @@ const module$1 = defineNuxtModule({
|
|
|
37
37
|
registerModule(MODULE_NAME, nuxt);
|
|
38
38
|
addAuthScope(
|
|
39
39
|
nuxt,
|
|
40
|
-
{ name: "subscriptions:read", description: "Read your subscription and plans" }
|
|
41
|
-
{ name: "subscriptions:write", description: "Start a subscription checkout" }
|
|
40
|
+
{ name: "subscriptions:read", description: "Read your subscription and plans" }
|
|
42
41
|
);
|
|
43
42
|
const resolver = createResolver(import.meta.url);
|
|
44
43
|
registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
|
|
@@ -87,44 +86,30 @@ const module$1 = defineNuxtModule({
|
|
|
87
86
|
handler: resolver.resolve("./runtime/server/stripe/invoice-events")
|
|
88
87
|
});
|
|
89
88
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
method: "get",
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
route: moduleApiUrl("subscriptions", "me"),
|
|
97
|
-
method: "get",
|
|
98
|
-
handler: resolver.resolve("./runtime/server/routes/subscriptions/me.get")
|
|
99
|
-
});
|
|
89
|
+
addModuleRoutes(resolver, [
|
|
90
|
+
{ method: "get", path: "plans", file: "subscriptions/plans.get" },
|
|
91
|
+
{ method: "get", path: "me", file: "subscriptions/me.get" },
|
|
92
|
+
{ method: "post", path: "me/checkout", file: "me/checkout.post" },
|
|
93
|
+
{ method: "post", path: "me/portal", file: "me/portal.post" }
|
|
94
|
+
], { base: "/api/subscriptions" });
|
|
100
95
|
registerBootstrapProvider(nuxt, {
|
|
101
96
|
scope: "account",
|
|
102
97
|
key: "subscriptions:me",
|
|
103
98
|
handler: resolver.resolve("./runtime/server/bootstrap/my-subscriptions")
|
|
104
99
|
});
|
|
105
|
-
addServerHandler({
|
|
106
|
-
route: moduleApiUrl("subscriptions", "checkout"),
|
|
107
|
-
method: "post",
|
|
108
|
-
handler: resolver.resolve("./runtime/server/routes/subscriptions/checkout.post")
|
|
109
|
-
});
|
|
110
|
-
addServerHandler({
|
|
111
|
-
route: moduleApiUrl("subscriptions", "portal"),
|
|
112
|
-
method: "post",
|
|
113
|
-
handler: resolver.resolve("./runtime/server/routes/subscriptions/portal.post")
|
|
114
|
-
});
|
|
115
100
|
await extendUsersAdmin(nuxt, {
|
|
116
101
|
routes: [
|
|
117
|
-
["get", "
|
|
118
|
-
["get", "
|
|
119
|
-
["post", "
|
|
120
|
-
["delete", "
|
|
121
|
-
["post", "
|
|
102
|
+
["get", "membership", "membership.get"],
|
|
103
|
+
["get", "membership/history", "membership-history.get"],
|
|
104
|
+
["post", "membership", "membership.post"],
|
|
105
|
+
["delete", "membership", "membership.delete"],
|
|
106
|
+
["post", "membership/payment-link", "payment-link.post"]
|
|
122
107
|
].map(([method, path, file]) => ({ method, path, handler: resolver.resolve(`./runtime/server/routes/admin/${file}`) })),
|
|
123
108
|
tab: {
|
|
124
|
-
id: "
|
|
109
|
+
id: "membership",
|
|
125
110
|
component: resolver.resolve("./runtime/app/tabs/Membership.vue"),
|
|
126
111
|
order: 10,
|
|
127
|
-
label: "
|
|
112
|
+
label: "Membership",
|
|
128
113
|
labelKey: "subscriptions.admin.tab",
|
|
129
114
|
icon: "i-lucide-badge-check"
|
|
130
115
|
},
|
|
@@ -34,12 +34,58 @@ declare var __VLS_14: {
|
|
|
34
34
|
pending: any;
|
|
35
35
|
select: () => void;
|
|
36
36
|
}, __VLS_35: {}, __VLS_37: {}, __VLS_66: {
|
|
37
|
-
plan:
|
|
38
|
-
|
|
37
|
+
plan: {
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description?: string | undefined;
|
|
41
|
+
rank: number;
|
|
42
|
+
features?: string[] | undefined;
|
|
43
|
+
prices: {
|
|
44
|
+
id: string;
|
|
45
|
+
amount: number | null;
|
|
46
|
+
currency: string;
|
|
47
|
+
interval: "day" | "week" | "month" | "year";
|
|
48
|
+
intervalCount: number;
|
|
49
|
+
trialPeriodDays: number | null;
|
|
50
|
+
lookupKey: string | null;
|
|
51
|
+
}[];
|
|
52
|
+
} | undefined;
|
|
53
|
+
price: {
|
|
54
|
+
id: string;
|
|
55
|
+
amount: number | null;
|
|
56
|
+
currency: string;
|
|
57
|
+
interval: "day" | "week" | "month" | "year";
|
|
58
|
+
intervalCount: number;
|
|
59
|
+
trialPeriodDays: number | null;
|
|
60
|
+
lookupKey: string | null;
|
|
61
|
+
} | undefined;
|
|
39
62
|
}, __VLS_74: {
|
|
40
|
-
plan:
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
plan: {
|
|
64
|
+
key: string;
|
|
65
|
+
label: string;
|
|
66
|
+
description?: string | undefined;
|
|
67
|
+
rank: number;
|
|
68
|
+
features?: string[] | undefined;
|
|
69
|
+
prices: {
|
|
70
|
+
id: string;
|
|
71
|
+
amount: number | null;
|
|
72
|
+
currency: string;
|
|
73
|
+
interval: "day" | "week" | "month" | "year";
|
|
74
|
+
intervalCount: number;
|
|
75
|
+
trialPeriodDays: number | null;
|
|
76
|
+
lookupKey: string | null;
|
|
77
|
+
}[];
|
|
78
|
+
} | undefined;
|
|
79
|
+
price: {
|
|
80
|
+
id: string;
|
|
81
|
+
amount: number | null;
|
|
82
|
+
currency: string;
|
|
83
|
+
interval: "day" | "week" | "month" | "year";
|
|
84
|
+
intervalCount: number;
|
|
85
|
+
trialPeriodDays: number | null;
|
|
86
|
+
lookupKey: string | null;
|
|
87
|
+
} | undefined;
|
|
88
|
+
confirm: () => Promise<void> | null;
|
|
43
89
|
};
|
|
44
90
|
type __VLS_Slots = {} & {
|
|
45
91
|
header?: (props: typeof __VLS_14) => any;
|
|
@@ -3,6 +3,7 @@ import { createReusableTemplate } from "@vueuse/core";
|
|
|
3
3
|
import { formatMoney } from "@frigear-nu/ui/money";
|
|
4
4
|
import { usePlans } from "../composables/usePlans";
|
|
5
5
|
import { useSubscription } from "../composables/useSubscription";
|
|
6
|
+
import { computed, navigateTo, ref, useAuthPages, useFormat, useModuleT, useRoute, useRuntimeConfig, useUserSession, watch, watchEffect } from "#imports";
|
|
6
7
|
defineOptions({ name: "SubscriptionPlans" });
|
|
7
8
|
const props = defineProps({
|
|
8
9
|
locale: { type: String, required: false },
|
|
@@ -54,8 +55,7 @@ const showGrid = computed(() => props.mode !== "tabs");
|
|
|
54
55
|
const activeTab = ref();
|
|
55
56
|
const tabItems = computed(() => visible.value.map((plan) => ({
|
|
56
57
|
value: plan.key,
|
|
57
|
-
label: priceFor(plan) ? `/ ${intervalLabel(priceFor(plan))}` : labelOf(plan)
|
|
58
|
-
plan
|
|
58
|
+
label: priceFor(plan) ? `/ ${intervalLabel(priceFor(plan))}` : labelOf(plan)
|
|
59
59
|
})));
|
|
60
60
|
watchEffect(() => {
|
|
61
61
|
if (!activeTab.value || !visible.value.some((p) => p.key === activeTab.value)) {
|
|
@@ -131,12 +131,10 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
131
131
|
:highlight="!preview && highlighted(plan)"
|
|
132
132
|
:badge="!preview && stateOf(plan) === 'current' ? t('current', 'Current plan') : void 0"
|
|
133
133
|
:tagline="priceFor(plan)?.trialPeriodDays ? t('trial', '{days}-day free trial', { days: priceFor(plan).trialPeriodDays }) : void 0"
|
|
134
|
-
variant="
|
|
134
|
+
variant="outline"
|
|
135
135
|
:ui="{
|
|
136
|
-
root: 'rounded-xl transition-shadow hover:shadow-lg',
|
|
137
136
|
title: 'text-lg font-semibold',
|
|
138
|
-
|
|
139
|
-
billingCycle: 'uppercase ml-1 text-muted',
|
|
137
|
+
billingCycle: 'ml-1 text-muted',
|
|
140
138
|
terms: 'text-xs text-muted'
|
|
141
139
|
}"
|
|
142
140
|
>
|
|
@@ -195,7 +193,6 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
195
193
|
<UButton
|
|
196
194
|
block
|
|
197
195
|
size="lg"
|
|
198
|
-
class="uppercase"
|
|
199
196
|
:loading="pending || loggedIn && subscriptionLoading"
|
|
200
197
|
:disabled="stateOf(plan) === 'current' || !priceFor(plan)"
|
|
201
198
|
:variant="stateOf(plan) === 'current' ? 'subtle' : 'solid'"
|
|
@@ -234,11 +231,9 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
234
231
|
|
|
235
232
|
<template v-if="visible.length">
|
|
236
233
|
<template v-if="showTabs">
|
|
237
|
-
<
|
|
234
|
+
<PageTabs
|
|
238
235
|
v-model="activeTab"
|
|
239
236
|
:items="tabItems"
|
|
240
|
-
:content="false"
|
|
241
|
-
class="w-full"
|
|
242
237
|
:class="{ 'sm:hidden': showGrid }"
|
|
243
238
|
/>
|
|
244
239
|
<div
|
|
@@ -253,7 +248,7 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
253
248
|
</template>
|
|
254
249
|
<div
|
|
255
250
|
v-if="showGrid"
|
|
256
|
-
class="items-stretch gap-
|
|
251
|
+
class="items-stretch gap-4 sm:grid-cols-2"
|
|
257
252
|
:class="[showTabs ? 'hidden sm:grid' : 'grid', visible.length >= 3 ? 'lg:grid-cols-3' : '']"
|
|
258
253
|
>
|
|
259
254
|
<ReuseCard
|
|
@@ -34,12 +34,58 @@ declare var __VLS_14: {
|
|
|
34
34
|
pending: any;
|
|
35
35
|
select: () => void;
|
|
36
36
|
}, __VLS_35: {}, __VLS_37: {}, __VLS_66: {
|
|
37
|
-
plan:
|
|
38
|
-
|
|
37
|
+
plan: {
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description?: string | undefined;
|
|
41
|
+
rank: number;
|
|
42
|
+
features?: string[] | undefined;
|
|
43
|
+
prices: {
|
|
44
|
+
id: string;
|
|
45
|
+
amount: number | null;
|
|
46
|
+
currency: string;
|
|
47
|
+
interval: "day" | "week" | "month" | "year";
|
|
48
|
+
intervalCount: number;
|
|
49
|
+
trialPeriodDays: number | null;
|
|
50
|
+
lookupKey: string | null;
|
|
51
|
+
}[];
|
|
52
|
+
} | undefined;
|
|
53
|
+
price: {
|
|
54
|
+
id: string;
|
|
55
|
+
amount: number | null;
|
|
56
|
+
currency: string;
|
|
57
|
+
interval: "day" | "week" | "month" | "year";
|
|
58
|
+
intervalCount: number;
|
|
59
|
+
trialPeriodDays: number | null;
|
|
60
|
+
lookupKey: string | null;
|
|
61
|
+
} | undefined;
|
|
39
62
|
}, __VLS_74: {
|
|
40
|
-
plan:
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
plan: {
|
|
64
|
+
key: string;
|
|
65
|
+
label: string;
|
|
66
|
+
description?: string | undefined;
|
|
67
|
+
rank: number;
|
|
68
|
+
features?: string[] | undefined;
|
|
69
|
+
prices: {
|
|
70
|
+
id: string;
|
|
71
|
+
amount: number | null;
|
|
72
|
+
currency: string;
|
|
73
|
+
interval: "day" | "week" | "month" | "year";
|
|
74
|
+
intervalCount: number;
|
|
75
|
+
trialPeriodDays: number | null;
|
|
76
|
+
lookupKey: string | null;
|
|
77
|
+
}[];
|
|
78
|
+
} | undefined;
|
|
79
|
+
price: {
|
|
80
|
+
id: string;
|
|
81
|
+
amount: number | null;
|
|
82
|
+
currency: string;
|
|
83
|
+
interval: "day" | "week" | "month" | "year";
|
|
84
|
+
intervalCount: number;
|
|
85
|
+
trialPeriodDays: number | null;
|
|
86
|
+
lookupKey: string | null;
|
|
87
|
+
} | undefined;
|
|
88
|
+
confirm: () => Promise<void> | null;
|
|
43
89
|
};
|
|
44
90
|
type __VLS_Slots = {} & {
|
|
45
91
|
header?: (props: typeof __VLS_14) => any;
|
|
@@ -7,12 +7,22 @@ declare var __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
|
7
7
|
subscription: any;
|
|
8
8
|
}, __VLS_15: {
|
|
9
9
|
subscription: any;
|
|
10
|
-
badge:
|
|
11
|
-
|
|
10
|
+
badge: {
|
|
11
|
+
status: any;
|
|
12
|
+
label: any;
|
|
13
|
+
color: any;
|
|
14
|
+
icon: string;
|
|
15
|
+
} | {
|
|
16
|
+
status: any;
|
|
17
|
+
label: any;
|
|
18
|
+
color: any;
|
|
19
|
+
icon: undefined;
|
|
20
|
+
};
|
|
21
|
+
}, __VLS_22: {
|
|
12
22
|
subscription: any;
|
|
13
23
|
cancelsAt: any;
|
|
14
24
|
dateText: any;
|
|
15
|
-
},
|
|
25
|
+
}, __VLS_24: {}, __VLS_32: {
|
|
16
26
|
manage: () => any;
|
|
17
27
|
resubscribe: () => any;
|
|
18
28
|
cancelling: boolean;
|
|
@@ -27,11 +37,11 @@ type __VLS_Slots = {} & {
|
|
|
27
37
|
} & {
|
|
28
38
|
badge?: (props: typeof __VLS_15) => any;
|
|
29
39
|
} & {
|
|
30
|
-
dates?: (props: typeof
|
|
40
|
+
dates?: (props: typeof __VLS_22) => any;
|
|
31
41
|
} & {
|
|
32
|
-
empty?: (props: typeof
|
|
42
|
+
empty?: (props: typeof __VLS_24) => any;
|
|
33
43
|
} & {
|
|
34
|
-
actions?: (props: typeof
|
|
44
|
+
actions?: (props: typeof __VLS_32) => any;
|
|
35
45
|
};
|
|
36
46
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
37
47
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { formatDate as formatDateValue } from "@frigear-nu/ui/utils";
|
|
3
3
|
import { usePlans } from "../composables/usePlans";
|
|
4
4
|
import { useSubscription } from "../composables/useSubscription";
|
|
5
|
+
import { computed, statusColor, useFormat, useModuleT } from "#imports";
|
|
5
6
|
defineOptions({ name: "SubscriptionStatus" });
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
locale: { type: String, required: false }
|
|
@@ -17,12 +18,12 @@ const planLabel = computed(() => {
|
|
|
17
18
|
});
|
|
18
19
|
const badge = computed(() => {
|
|
19
20
|
const value = subscription.value?.status;
|
|
20
|
-
if (cancelsAt.value) return { label: t("cancelling", "
|
|
21
|
-
if (value === "active") return { label: t("active", "Active"), color: statusColor(value), icon: "i-lucide-shield-check" };
|
|
22
|
-
if (value === "trialing") return { label: t("trialing", "Trial"), color: statusColor(value) };
|
|
23
|
-
if (value === "past_due" || value === "unpaid") return { label: t("pastDue", "Past due"), color: statusColor("past_due") };
|
|
24
|
-
if (value === "canceled") return { label: t("canceled", "Canceled"), color: statusColor(value) };
|
|
25
|
-
return { label: t("other", "Inactive"), color: statusColor(value) };
|
|
21
|
+
if (cancelsAt.value) return { status: "cancelling", label: t("cancelling", "Membership is ending"), color: "warning", icon: "i-lucide-battery-warning" };
|
|
22
|
+
if (value === "active") return { status: value, label: t("active", "Active"), color: statusColor(value), icon: "i-lucide-shield-check" };
|
|
23
|
+
if (value === "trialing") return { status: value, label: t("trialing", "Trial"), color: statusColor(value), icon: void 0 };
|
|
24
|
+
if (value === "past_due" || value === "unpaid") return { status: "past_due", label: t("pastDue", "Past due"), color: statusColor("past_due"), icon: void 0 };
|
|
25
|
+
if (value === "canceled") return { status: value, label: t("canceled", "Canceled"), color: statusColor(value), icon: void 0 };
|
|
26
|
+
return { status: value, label: t("other", "Inactive"), color: statusColor(value), icon: void 0 };
|
|
26
27
|
});
|
|
27
28
|
const formatDate = (value) => formatDateValue(value, props.locale ?? format.locale.value, { dateStyle: "long" });
|
|
28
29
|
const isManual = computed(() => subscription.value?.source === "manual");
|
|
@@ -38,8 +39,9 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
38
39
|
|
|
39
40
|
<template>
|
|
40
41
|
<SectionCard
|
|
41
|
-
:title="t('title', 'Your
|
|
42
|
+
:title="t('title', 'Your membership')"
|
|
42
43
|
icon="i-lucide-credit-card"
|
|
44
|
+
:loading="loading && !$slots.loading"
|
|
43
45
|
>
|
|
44
46
|
<template
|
|
45
47
|
v-if="$slots.header"
|
|
@@ -53,7 +55,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
53
55
|
name="loading"
|
|
54
56
|
>
|
|
55
57
|
<p class="text-muted text-sm">
|
|
56
|
-
{{ t("loading", "Loading
|
|
58
|
+
{{ t("loading", "Loading membership\u2026") }}
|
|
57
59
|
</p>
|
|
58
60
|
</slot>
|
|
59
61
|
|
|
@@ -78,13 +80,12 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
78
80
|
:subscription="subscription"
|
|
79
81
|
:badge="badge"
|
|
80
82
|
>
|
|
81
|
-
<
|
|
82
|
-
:
|
|
83
|
+
<StatusBadge
|
|
84
|
+
:status="badge.status"
|
|
85
|
+
:label="badge.label"
|
|
83
86
|
:icon="badge.icon"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{{ badge.label }}
|
|
87
|
-
</UBadge>
|
|
87
|
+
:colors="{ cancelling: 'warning' }"
|
|
88
|
+
/>
|
|
88
89
|
</slot>
|
|
89
90
|
</div>
|
|
90
91
|
<slot
|
|
@@ -107,7 +108,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
107
108
|
name="empty"
|
|
108
109
|
>
|
|
109
110
|
<p class="text-muted text-sm">
|
|
110
|
-
{{ t("none", "You do not have an active
|
|
111
|
+
{{ t("none", "You do not have an active membership.") }}
|
|
111
112
|
</p>
|
|
112
113
|
</slot>
|
|
113
114
|
|
|
@@ -130,23 +131,16 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
130
131
|
:cancelling="!!cancelsAt"
|
|
131
132
|
:pending="pending"
|
|
132
133
|
>
|
|
133
|
-
<div class="flex flex-wrap gap-
|
|
134
|
+
<div class="flex flex-wrap justify-end gap-2">
|
|
134
135
|
<UButton
|
|
135
136
|
v-if="!cancelsAt"
|
|
136
137
|
color="error"
|
|
137
|
-
variant="
|
|
138
|
+
variant="outline"
|
|
138
139
|
icon="i-lucide-x"
|
|
139
140
|
:loading="pending"
|
|
140
141
|
:label="t('cancel', 'Cancel membership')"
|
|
141
142
|
@click="manage"
|
|
142
143
|
/>
|
|
143
|
-
<UButton
|
|
144
|
-
v-else
|
|
145
|
-
icon="i-lucide-rotate-ccw"
|
|
146
|
-
:loading="pending"
|
|
147
|
-
:label="t('resubscribe', 'Resubscribe')"
|
|
148
|
-
@click="resubscribe"
|
|
149
|
-
/>
|
|
150
144
|
<UButton
|
|
151
145
|
color="neutral"
|
|
152
146
|
variant="outline"
|
|
@@ -155,6 +149,13 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
155
149
|
:label="t('changePaymentMethod', 'Change payment method')"
|
|
156
150
|
@click="manage"
|
|
157
151
|
/>
|
|
152
|
+
<UButton
|
|
153
|
+
v-if="cancelsAt"
|
|
154
|
+
icon="i-lucide-rotate-ccw"
|
|
155
|
+
:loading="pending"
|
|
156
|
+
:label="t('resubscribe', 'Resubscribe')"
|
|
157
|
+
@click="resubscribe"
|
|
158
|
+
/>
|
|
158
159
|
</div>
|
|
159
160
|
</slot>
|
|
160
161
|
</template>
|
|
@@ -7,12 +7,22 @@ declare var __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
|
7
7
|
subscription: any;
|
|
8
8
|
}, __VLS_15: {
|
|
9
9
|
subscription: any;
|
|
10
|
-
badge:
|
|
11
|
-
|
|
10
|
+
badge: {
|
|
11
|
+
status: any;
|
|
12
|
+
label: any;
|
|
13
|
+
color: any;
|
|
14
|
+
icon: string;
|
|
15
|
+
} | {
|
|
16
|
+
status: any;
|
|
17
|
+
label: any;
|
|
18
|
+
color: any;
|
|
19
|
+
icon: undefined;
|
|
20
|
+
};
|
|
21
|
+
}, __VLS_22: {
|
|
12
22
|
subscription: any;
|
|
13
23
|
cancelsAt: any;
|
|
14
24
|
dateText: any;
|
|
15
|
-
},
|
|
25
|
+
}, __VLS_24: {}, __VLS_32: {
|
|
16
26
|
manage: () => any;
|
|
17
27
|
resubscribe: () => any;
|
|
18
28
|
cancelling: boolean;
|
|
@@ -27,11 +37,11 @@ type __VLS_Slots = {} & {
|
|
|
27
37
|
} & {
|
|
28
38
|
badge?: (props: typeof __VLS_15) => any;
|
|
29
39
|
} & {
|
|
30
|
-
dates?: (props: typeof
|
|
40
|
+
dates?: (props: typeof __VLS_22) => any;
|
|
31
41
|
} & {
|
|
32
|
-
empty?: (props: typeof
|
|
42
|
+
empty?: (props: typeof __VLS_24) => any;
|
|
33
43
|
} & {
|
|
34
|
-
actions?: (props: typeof
|
|
44
|
+
actions?: (props: typeof __VLS_32) => any;
|
|
35
45
|
};
|
|
36
46
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
37
47
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { subscriptionsApi } from "../../../../utils/api";
|
|
3
|
+
import { $fetch, computed, ref, useAction, useConfirmDialog, useCopyToClipboard, useFormat, useLazyFetch, useModuleT, useToast, watchEffect } from "#imports";
|
|
2
4
|
defineOptions({ name: "UsersAdminMembership" });
|
|
3
5
|
const props = defineProps({
|
|
4
6
|
user: { type: Object, required: true }
|
|
@@ -12,7 +14,7 @@ const toast = useToast();
|
|
|
12
14
|
const grantAction = useAction();
|
|
13
15
|
const linkAction = useAction();
|
|
14
16
|
const revokeAction = useAction();
|
|
15
|
-
const base = computed(() =>
|
|
17
|
+
const base = computed(() => subscriptionsApi.admin.membership(props.user.id));
|
|
16
18
|
const { data, refresh, status } = useLazyFetch(() => base.value, { key: () => `admin-membership-${props.user.id}` });
|
|
17
19
|
const loading = computed(() => status.value === "pending" && !data.value);
|
|
18
20
|
const planKey = ref(void 0);
|
|
@@ -60,7 +62,7 @@ async function revoke() {
|
|
|
60
62
|
}, { success: t("revoked", "Membership revoked."), error: t("error", "Something went wrong.") });
|
|
61
63
|
}
|
|
62
64
|
async function sendLink() {
|
|
63
|
-
const result = await linkAction.run(() => $fetch(
|
|
65
|
+
const result = await linkAction.run(() => $fetch(subscriptionsApi.admin.paymentLink(props.user.id), {
|
|
64
66
|
method: "POST",
|
|
65
67
|
body: { planKey: planKey.value, email: emailUser.value }
|
|
66
68
|
}), { error: t("error", "Something went wrong.") });
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { subscriptionsApi } from "../../utils/api.js";
|
|
2
|
+
import { computed, useCachedFetch } from "#imports";
|
|
1
3
|
export const PLANS_CLIENT_CACHE_MS = 5 * 60 * 1e3;
|
|
2
4
|
export const PLANS_KEY = "subscriptions:plans";
|
|
3
5
|
export const usePlans = () => {
|
|
4
|
-
const fetched = useCachedFetch(
|
|
6
|
+
const fetched = useCachedFetch(subscriptionsApi.plans, {
|
|
5
7
|
key: PLANS_KEY,
|
|
6
8
|
ttl: PLANS_CLIENT_CACHE_MS
|
|
7
9
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { subscriptionsApi } from "../../utils/api.js";
|
|
2
|
+
import { $fetch, computed, navigateTo, ref, toValue, useCachedFetch, useModuleT, useRuntimeConfig, useUserSession, watch } from "#imports";
|
|
1
3
|
const ACTIVE_STATUSES = ["active", "trialing"];
|
|
2
4
|
const CLIENT_CACHE_MS = 60 * 1e3;
|
|
3
5
|
const ME_KEY = "subscriptions:me";
|
|
@@ -10,7 +12,7 @@ export const useSubscription = () => {
|
|
|
10
12
|
const t = useModuleT("subscriptions");
|
|
11
13
|
const pending = ref(false);
|
|
12
14
|
const error = ref(null);
|
|
13
|
-
const fetched = useCachedFetch(
|
|
15
|
+
const fetched = useCachedFetch(subscriptionsApi.me.subscriptions, {
|
|
14
16
|
key: ME_KEY,
|
|
15
17
|
ttl: CLIENT_CACHE_MS,
|
|
16
18
|
immediate: loggedIn.value,
|
|
@@ -51,7 +53,7 @@ export const useSubscription = () => {
|
|
|
51
53
|
const origin = import.meta.client ? window.location.origin : "";
|
|
52
54
|
const absolute = (path) => path ? `${origin}${path}` : void 0;
|
|
53
55
|
return run(async () => {
|
|
54
|
-
const result = await $fetch(
|
|
56
|
+
const result = await $fetch(subscriptionsApi.me.checkout, {
|
|
55
57
|
method: "POST",
|
|
56
58
|
body: {
|
|
57
59
|
...isPrice ? { priceId: planKeyOrPriceId } : { planKey: planKeyOrPriceId },
|
|
@@ -69,7 +71,7 @@ export const useSubscription = () => {
|
|
|
69
71
|
}
|
|
70
72
|
function openPortal(returnUrl) {
|
|
71
73
|
return run(async () => {
|
|
72
|
-
const { url } = await $fetch(
|
|
74
|
+
const { url } = await $fetch(subscriptionsApi.me.portal, {
|
|
73
75
|
method: "POST",
|
|
74
76
|
body: { returnUrl: returnUrl ?? (import.meta.client ? window.location.href : void 0) }
|
|
75
77
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nuxt/app").RouteMiddleware;
|
|
2
2
|
export default _default;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, useLocaleLink, useModuleT, useRuntimeConfig } from "#imports";
|
|
2
3
|
defineOptions({ name: "SubscriptionCancelPage" });
|
|
3
4
|
const t = useModuleT("subscriptions.cancel");
|
|
4
5
|
const { localize } = useLocaleLink();
|
|
6
|
+
const tm = useModuleT("subscriptions.menu.membership");
|
|
5
7
|
const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
|
|
8
|
+
const breadcrumb = computed(() => [
|
|
9
|
+
...membership ? [{ label: tm("label", "Membership"), to: localize(membership) }] : [],
|
|
10
|
+
{ label: t("crumb", "Checkout canceled") }
|
|
11
|
+
]);
|
|
6
12
|
</script>
|
|
7
13
|
|
|
8
14
|
<template>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
<div class="flex flex-col gap-4">
|
|
16
|
+
<PageHeading
|
|
17
|
+
:title="tm('label', 'Membership')"
|
|
18
|
+
:description="tm('description', 'Manage your plan and billing.')"
|
|
19
|
+
:breadcrumb="breadcrumb"
|
|
20
|
+
/>
|
|
21
|
+
<UEmpty
|
|
22
|
+
icon="i-lucide-circle-x"
|
|
23
|
+
:title="t('title', 'Checkout canceled')"
|
|
24
|
+
:description="t('body', 'No changes were made and you have not been charged.')"
|
|
25
|
+
:actions="membership ? [{ label: t('back', 'Back to membership'), to: localize(membership) }] : []"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
15
28
|
</template>
|