@frigear-nu/subscriptions 0.0.10 → 0.1.1
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.json +1 -1
- package/dist/module.mjs +12 -4
- package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +4 -4
- package/dist/runtime/app/components/SubscriptionPlans.vue +4 -1
- package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +4 -4
- package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +7 -2
- package/dist/runtime/app/components/SubscriptionStatus.vue +25 -22
- package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +7 -2
- package/dist/runtime/app/components/Users/Admin/Membership.d.vue.ts +3 -3
- package/dist/runtime/app/components/Users/Admin/Membership.vue +90 -94
- package/dist/runtime/app/components/Users/Admin/Membership.vue.d.ts +3 -3
- package/dist/runtime/app/composables/useSubscription.d.ts +8 -1
- package/dist/runtime/app/pages/cancel.d.vue.ts +1 -1
- package/dist/runtime/app/pages/cancel.vue +7 -15
- 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 +21 -23
- 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 +7 -17
- package/dist/runtime/app/pages/success.vue.d.ts +1 -1
- package/dist/runtime/app/tabs/Membership.d.vue.ts +3 -3
- package/dist/runtime/app/tabs/Membership.vue +26 -21
- package/dist/runtime/app/tabs/Membership.vue.d.ts +3 -3
- package/dist/runtime/app/widgets/AdminCurrentSubscription.d.vue.ts +1 -1
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue +9 -5
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue.d.ts +1 -1
- package/dist/runtime/app/widgets/MembershipSummary.d.vue.ts +3 -0
- package/dist/runtime/app/widgets/MembershipSummary.vue +36 -0
- package/dist/runtime/app/widgets/MembershipSummary.vue.d.ts +3 -0
- package/dist/runtime/emails/subscriptions-payment-link.d.vue.ts +8 -0
- package/dist/runtime/emails/subscriptions-payment-link.vue +34 -0
- package/dist/runtime/emails/subscriptions-payment-link.vue.d.ts +8 -0
- package/dist/runtime/i18n/lang/da.json +16 -4
- package/dist/runtime/i18n/lang/en.json +16 -4
- package/dist/runtime/menus.js +3 -5
- package/dist/runtime/server/routes/admin/payment-link.post.js +2 -1
- package/package.json +24 -18
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -21,12 +21,16 @@ const module$1 = defineNuxtModule({
|
|
|
21
21
|
"@frigear-nu/auth": { version: ">=0.0.1" },
|
|
22
22
|
"@frigear-nu/mcp": { version: ">=0.0.1" },
|
|
23
23
|
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
24
|
+
// The payment link email is dispatched through it.
|
|
25
|
+
"@frigear-nu/notify": { version: ">=0.0.1" },
|
|
24
26
|
"@frigear-nu/stripe": { version: ">=0.0.1" },
|
|
27
|
+
// The pages and admin pieces are built from its components and composables (`SectionCard`, `useFormat`, ...).
|
|
28
|
+
"@frigear-nu/ui": { version: ">=0.0.1" },
|
|
25
29
|
"@frigear-nu/menu": { version: ">=0.0.1", optional: true },
|
|
26
30
|
// Membership invoices are only announced (and stored as payments) when it is installed.
|
|
27
31
|
"@frigear-nu/payments": { version: ">=0.0.0", optional: true },
|
|
28
32
|
"@frigear-nu/widget": { version: ">=0.0.0", optional: true },
|
|
29
|
-
// The admin user tab and card
|
|
33
|
+
// The admin user tab and card live on its admin user page, so they are only added with it.
|
|
30
34
|
"@frigear-nu/admin": { version: ">=0.0.0", optional: true }
|
|
31
35
|
},
|
|
32
36
|
async setup(options, nuxt) {
|
|
@@ -63,8 +67,7 @@ const module$1 = defineNuxtModule({
|
|
|
63
67
|
plans: planEntries.map(([key, plan]) => ({ key, label: plan.label ?? key, description: plan.description, rank: plan.rank, features: plan.features })).sort((a, b) => a.rank - b.rank),
|
|
64
68
|
allowChangeInPortal: settings.allowChangeInPortal === true
|
|
65
69
|
};
|
|
66
|
-
|
|
67
|
-
name: "subscriptions",
|
|
70
|
+
registerDatabaseTable(nuxt, {
|
|
68
71
|
resolver,
|
|
69
72
|
schema: (dialect) => `./runtime/server/db/schema.${dialect}`
|
|
70
73
|
});
|
|
@@ -127,6 +130,7 @@ const module$1 = defineNuxtModule({
|
|
|
127
130
|
span: 2
|
|
128
131
|
}
|
|
129
132
|
});
|
|
133
|
+
nuxt.hook("notify:email:templates", (sources) => sources.push({ dir: resolver.resolve("./runtime/emails") }));
|
|
130
134
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
131
135
|
inlineModuleRuntime(nuxt, resolver);
|
|
132
136
|
if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
@@ -145,7 +149,11 @@ const module$1 = defineNuxtModule({
|
|
|
145
149
|
{ name: "subscriptions-success", path: successPath, file: "./runtime/app/pages/success.vue" },
|
|
146
150
|
{ name: "subscriptions-cancel", path: cancelPath, file: "./runtime/app/pages/cancel.vue" }
|
|
147
151
|
]);
|
|
148
|
-
addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "
|
|
152
|
+
addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "account" });
|
|
153
|
+
if (membershipPath && hasNuxtModule("@frigear-nu/widget", nuxt)) {
|
|
154
|
+
const { extendWidgets } = await import('@frigear-nu/widget/utils');
|
|
155
|
+
extendWidgets(nuxt, "account.overview", { id: "subscriptions-membership", component: resolver.resolve("./runtime/app/widgets/MembershipSummary.vue"), order: 10 });
|
|
156
|
+
}
|
|
149
157
|
}
|
|
150
158
|
});
|
|
151
159
|
|
|
@@ -13,7 +13,7 @@ type __VLS_Props = {
|
|
|
13
13
|
/** Plan key to emphasise (defaults to the current plan, when subscribed). */
|
|
14
14
|
highlight?: string;
|
|
15
15
|
};
|
|
16
|
-
declare function formatPrice(price: SubscriptionPlanPrice):
|
|
16
|
+
declare function formatPrice(price: SubscriptionPlanPrice): any;
|
|
17
17
|
declare function intervalLabel(price: SubscriptionPlanPrice): any;
|
|
18
18
|
declare var __VLS_14: {
|
|
19
19
|
plan: SubscriptionPlan;
|
|
@@ -60,11 +60,11 @@ type __VLS_Slots = {} & {
|
|
|
60
60
|
} & {
|
|
61
61
|
'confirm-actions'?: (props: typeof __VLS_74) => any;
|
|
62
62
|
};
|
|
63
|
-
declare const __VLS_base: import("
|
|
63
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
64
64
|
select: (plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any;
|
|
65
|
-
}, string, import("
|
|
65
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
66
66
|
onSelect?: ((plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any) | undefined;
|
|
67
|
-
}>, {}, {}, {}, {}, string, import("
|
|
67
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
68
68
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
69
69
|
declare const _default: typeof __VLS_export;
|
|
70
70
|
export default _default;
|
|
@@ -20,6 +20,7 @@ const ti = useModuleT("subscriptions.interval");
|
|
|
20
20
|
const { plans: allPlans, status } = usePlans();
|
|
21
21
|
const { plan: currentPlan, isActive, cancelsAt, pending, subscribe, error, status: subscriptionStatus } = useSubscription();
|
|
22
22
|
const { loggedIn } = useUserSession();
|
|
23
|
+
const format = useFormat();
|
|
23
24
|
const route = useRoute();
|
|
24
25
|
const [DefineCard, ReuseCard] = createReusableTemplate();
|
|
25
26
|
const visible = computed(() => allPlans.value.filter((p) => !props.plans || props.plans.includes(p.key)));
|
|
@@ -29,7 +30,8 @@ function priceFor(plan) {
|
|
|
29
30
|
}
|
|
30
31
|
function formatPrice(price) {
|
|
31
32
|
if (price.amount === null) return "";
|
|
32
|
-
|
|
33
|
+
const currency = props.currency ?? price.currency;
|
|
34
|
+
return props.locale ? formatMoney(price.amount, currency, props.locale) : format.money(price.amount, currency);
|
|
33
35
|
}
|
|
34
36
|
function intervalLabel(price) {
|
|
35
37
|
const singular = { day: "day", week: "week", month: "month", year: "year" };
|
|
@@ -296,6 +298,7 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
296
298
|
>
|
|
297
299
|
<div class="flex w-full justify-end gap-2">
|
|
298
300
|
<UButton
|
|
301
|
+
color="neutral"
|
|
299
302
|
variant="outline"
|
|
300
303
|
:label="t('cancel', 'Cancel')"
|
|
301
304
|
@click="confirming = null"
|
|
@@ -13,7 +13,7 @@ type __VLS_Props = {
|
|
|
13
13
|
/** Plan key to emphasise (defaults to the current plan, when subscribed). */
|
|
14
14
|
highlight?: string;
|
|
15
15
|
};
|
|
16
|
-
declare function formatPrice(price: SubscriptionPlanPrice):
|
|
16
|
+
declare function formatPrice(price: SubscriptionPlanPrice): any;
|
|
17
17
|
declare function intervalLabel(price: SubscriptionPlanPrice): any;
|
|
18
18
|
declare var __VLS_14: {
|
|
19
19
|
plan: SubscriptionPlan;
|
|
@@ -60,11 +60,11 @@ type __VLS_Slots = {} & {
|
|
|
60
60
|
} & {
|
|
61
61
|
'confirm-actions'?: (props: typeof __VLS_74) => any;
|
|
62
62
|
};
|
|
63
|
-
declare const __VLS_base: import("
|
|
63
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
64
64
|
select: (plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any;
|
|
65
|
-
}, string, import("
|
|
65
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
66
66
|
onSelect?: ((plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any) | undefined;
|
|
67
|
-
}>, {}, {}, {}, {}, string, import("
|
|
67
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
68
68
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
69
69
|
declare const _default: typeof __VLS_export;
|
|
70
70
|
export default _default;
|
|
@@ -14,7 +14,12 @@ declare var __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
|
14
14
|
dateText: any;
|
|
15
15
|
}, __VLS_25: {}, __VLS_33: {
|
|
16
16
|
manage: () => Promise<void | undefined>;
|
|
17
|
-
resubscribe: () => Promise<
|
|
17
|
+
resubscribe: () => Promise<({
|
|
18
|
+
url: string;
|
|
19
|
+
} | {
|
|
20
|
+
updated: true;
|
|
21
|
+
kind: "upgrade" | "downgrade" | "resume";
|
|
22
|
+
}) | undefined>;
|
|
18
23
|
cancelling: boolean;
|
|
19
24
|
pending: any;
|
|
20
25
|
};
|
|
@@ -33,7 +38,7 @@ type __VLS_Slots = {} & {
|
|
|
33
38
|
} & {
|
|
34
39
|
actions?: (props: typeof __VLS_33) => any;
|
|
35
40
|
};
|
|
36
|
-
declare const __VLS_base: import("
|
|
41
|
+
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
42
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
38
43
|
declare const _default: typeof __VLS_export;
|
|
39
44
|
export default _default;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { formatDate as formatDateValue } from "@frigear-nu/ui/utils";
|
|
2
3
|
import { usePlans } from "../composables/usePlans";
|
|
3
4
|
import { useSubscription } from "../composables/useSubscription";
|
|
4
5
|
defineOptions({ name: "SubscriptionStatus" });
|
|
@@ -7,6 +8,7 @@ const props = defineProps({
|
|
|
7
8
|
});
|
|
8
9
|
const t = useModuleT("subscriptions.status");
|
|
9
10
|
const tp = useModuleT("subscriptions.catalog");
|
|
11
|
+
const format = useFormat();
|
|
10
12
|
const { plans } = usePlans();
|
|
11
13
|
const { subscription, plan, status, isActive, cancelsAt, pending, error, subscribe, openPortal } = useSubscription();
|
|
12
14
|
const planLabel = computed(() => {
|
|
@@ -16,15 +18,13 @@ const planLabel = computed(() => {
|
|
|
16
18
|
const badge = computed(() => {
|
|
17
19
|
const value = subscription.value?.status;
|
|
18
20
|
if (cancelsAt.value) return { label: t("cancelling", "Subscription is ending"), color: "warning", icon: "i-lucide-battery-warning" };
|
|
19
|
-
if (value === "active") return { label: t("active", "Active"), color:
|
|
20
|
-
if (value === "trialing") return { label: t("trialing", "Trial"), color:
|
|
21
|
-
if (value === "past_due" || value === "unpaid") return { label: t("pastDue", "Past due"), color: "
|
|
22
|
-
if (value === "canceled") return { label: t("canceled", "Canceled"), color:
|
|
23
|
-
return { label: t("other", "Inactive"), color:
|
|
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) };
|
|
24
26
|
});
|
|
25
|
-
|
|
26
|
-
return value ? new Intl.DateTimeFormat(props.locale, { dateStyle: "long" }).format(new Date(value)) : "";
|
|
27
|
-
}
|
|
27
|
+
const formatDate = (value) => formatDateValue(value, props.locale ?? format.locale.value, { dateStyle: "long" });
|
|
28
28
|
const isManual = computed(() => subscription.value?.source === "manual");
|
|
29
29
|
const dateText = computed(() => {
|
|
30
30
|
const end = subscription.value?.currentPeriodEnd;
|
|
@@ -37,20 +37,22 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
37
37
|
</script>
|
|
38
38
|
|
|
39
39
|
<template>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
<SectionCard
|
|
41
|
+
:title="t('title', 'Your subscription')"
|
|
42
|
+
icon="i-lucide-credit-card"
|
|
43
|
+
>
|
|
44
|
+
<template
|
|
45
|
+
v-if="$slots.header"
|
|
46
|
+
#header
|
|
47
|
+
>
|
|
48
|
+
<slot name="header" />
|
|
47
49
|
</template>
|
|
48
50
|
|
|
49
51
|
<slot
|
|
50
52
|
v-if="status === 'pending' && !subscription"
|
|
51
53
|
name="loading"
|
|
52
54
|
>
|
|
53
|
-
<p class="text-
|
|
55
|
+
<p class="text-muted text-sm">
|
|
54
56
|
{{ t("loading", "Loading subscription\u2026") }}
|
|
55
57
|
</p>
|
|
56
58
|
</slot>
|
|
@@ -61,14 +63,14 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
61
63
|
>
|
|
62
64
|
<div class="flex items-center justify-between gap-2">
|
|
63
65
|
<div class="flex flex-col">
|
|
64
|
-
<span class="text-
|
|
66
|
+
<span class="text-muted text-sm">{{ t("currentPlan", "Current plan") }}</span>
|
|
65
67
|
<slot
|
|
66
68
|
name="plan"
|
|
67
69
|
:plan="plan"
|
|
68
70
|
:label="planLabel"
|
|
69
71
|
:subscription="subscription"
|
|
70
72
|
>
|
|
71
|
-
<span class="text-lg font-semibold">{{ planLabel }}</span>
|
|
73
|
+
<span class="text-highlighted text-lg font-semibold">{{ planLabel }}</span>
|
|
72
74
|
</slot>
|
|
73
75
|
</div>
|
|
74
76
|
<slot
|
|
@@ -79,7 +81,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
79
81
|
<UBadge
|
|
80
82
|
:color="badge.color"
|
|
81
83
|
:icon="badge.icon"
|
|
82
|
-
variant="
|
|
84
|
+
variant="subtle"
|
|
83
85
|
>
|
|
84
86
|
{{ badge.label }}
|
|
85
87
|
</UBadge>
|
|
@@ -93,7 +95,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
93
95
|
>
|
|
94
96
|
<div
|
|
95
97
|
v-if="dateText"
|
|
96
|
-
class="
|
|
98
|
+
class="bg-muted/50 rounded-md px-4 py-3 text-sm"
|
|
97
99
|
>
|
|
98
100
|
{{ dateText }}
|
|
99
101
|
</div>
|
|
@@ -104,7 +106,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
104
106
|
v-else
|
|
105
107
|
name="empty"
|
|
106
108
|
>
|
|
107
|
-
<p class="text-
|
|
109
|
+
<p class="text-muted text-sm">
|
|
108
110
|
{{ t("none", "You do not have an active subscription.") }}
|
|
109
111
|
</p>
|
|
110
112
|
</slot>
|
|
@@ -146,6 +148,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
146
148
|
@click="resubscribe"
|
|
147
149
|
/>
|
|
148
150
|
<UButton
|
|
151
|
+
color="neutral"
|
|
149
152
|
variant="outline"
|
|
150
153
|
icon="i-lucide-credit-card"
|
|
151
154
|
:loading="pending"
|
|
@@ -155,5 +158,5 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
155
158
|
</div>
|
|
156
159
|
</slot>
|
|
157
160
|
</template>
|
|
158
|
-
</
|
|
161
|
+
</SectionCard>
|
|
159
162
|
</template>
|
|
@@ -14,7 +14,12 @@ declare var __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
|
14
14
|
dateText: any;
|
|
15
15
|
}, __VLS_25: {}, __VLS_33: {
|
|
16
16
|
manage: () => Promise<void | undefined>;
|
|
17
|
-
resubscribe: () => Promise<
|
|
17
|
+
resubscribe: () => Promise<({
|
|
18
|
+
url: string;
|
|
19
|
+
} | {
|
|
20
|
+
updated: true;
|
|
21
|
+
kind: "upgrade" | "downgrade" | "resume";
|
|
22
|
+
}) | undefined>;
|
|
18
23
|
cancelling: boolean;
|
|
19
24
|
pending: any;
|
|
20
25
|
};
|
|
@@ -33,7 +38,7 @@ type __VLS_Slots = {} & {
|
|
|
33
38
|
} & {
|
|
34
39
|
actions?: (props: typeof __VLS_33) => any;
|
|
35
40
|
};
|
|
36
|
-
declare const __VLS_base: import("
|
|
41
|
+
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
42
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
38
43
|
declare const _default: typeof __VLS_export;
|
|
39
44
|
export default _default;
|
|
@@ -5,10 +5,10 @@ type __VLS_Props = {
|
|
|
5
5
|
email: string;
|
|
6
6
|
};
|
|
7
7
|
};
|
|
8
|
-
declare const __VLS_export: import("
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
9
|
refresh: () => any;
|
|
10
|
-
}, string, import("
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
11
11
|
onRefresh?: (() => any) | undefined;
|
|
12
|
-
}>, {}, {}, {}, {}, string, import("
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
13
|
declare const _default: typeof __VLS_export;
|
|
14
14
|
export default _default;
|
|
@@ -5,7 +5,13 @@ const props = defineProps({
|
|
|
5
5
|
});
|
|
6
6
|
const emit = defineEmits(["refresh"]);
|
|
7
7
|
const t = useModuleT("subscriptions.admin");
|
|
8
|
+
const format = useFormat();
|
|
9
|
+
const confirm = useConfirmDialog();
|
|
10
|
+
const { copy } = useCopyToClipboard();
|
|
8
11
|
const toast = useToast();
|
|
12
|
+
const grantAction = useAction();
|
|
13
|
+
const linkAction = useAction();
|
|
14
|
+
const revokeAction = useAction();
|
|
9
15
|
const base = computed(() => `/api/admin/users/${props.user.id}/subscription`);
|
|
10
16
|
const { data, refresh, status } = useLazyFetch(() => base.value, { key: () => `admin-membership-${props.user.id}` });
|
|
11
17
|
const loading = computed(() => status.value === "pending" && !data.value);
|
|
@@ -13,106 +19,100 @@ const planKey = ref(void 0);
|
|
|
13
19
|
const endsAt = ref("");
|
|
14
20
|
const emailUser = ref(true);
|
|
15
21
|
const link = ref(null);
|
|
16
|
-
const busy = ref(null);
|
|
17
22
|
const planItems = computed(() => (data.value?.plans ?? []).map((plan) => ({ label: plan.label, value: plan.key })));
|
|
18
23
|
watchEffect(() => {
|
|
19
24
|
if (!planKey.value && planItems.value.length) planKey.value = planItems.value[0].value;
|
|
20
25
|
});
|
|
21
26
|
const current = computed(() => data.value?.current ?? null);
|
|
22
27
|
const planLabel = (key) => planItems.value.find((item) => item.value === key)?.label ?? key ?? "\u2013";
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
const details = computed(() => [
|
|
29
|
+
{ key: "plan", label: t("plan", "Plan") },
|
|
30
|
+
{ key: "type", label: t("type", "Type"), value: current.value?.source === "manual" ? t("free", "Free of charge") : t("paid", "Paid (Stripe)") },
|
|
31
|
+
{
|
|
32
|
+
key: "validUntil",
|
|
33
|
+
label: t("validUntil", "Valid until"),
|
|
34
|
+
value: current.value?.currentPeriodEnd ? format.date(current.value.currentPeriodEnd) : t("openEnded", "No end date")
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
27
37
|
async function changed() {
|
|
28
38
|
await refresh();
|
|
29
39
|
emit("refresh");
|
|
30
40
|
}
|
|
31
|
-
|
|
32
|
-
busy.value = kind;
|
|
33
|
-
try {
|
|
34
|
-
await action();
|
|
35
|
-
} catch (cause) {
|
|
36
|
-
fail(cause);
|
|
37
|
-
} finally {
|
|
38
|
-
busy.value = null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const grant = () => run("grant", async () => {
|
|
41
|
+
const grant = () => grantAction.run(async () => {
|
|
42
42
|
await $fetch(base.value, {
|
|
43
43
|
method: "POST",
|
|
44
44
|
body: { planKey: planKey.value, endsAt: endsAt.value ? (/* @__PURE__ */ new Date(`${endsAt.value}T23:59:59`)).toISOString() : null }
|
|
45
45
|
});
|
|
46
46
|
link.value = null;
|
|
47
|
-
toast.add({ title: t("granted", "Membership granted."), color: "success" });
|
|
48
47
|
await changed();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
}, { success: t("granted", "Membership granted."), error: t("error", "Something went wrong.") });
|
|
49
|
+
async function revoke() {
|
|
50
|
+
const confirmed = await confirm({
|
|
51
|
+
title: t("revokeTitle", "Revoke the free membership of {name}?", { name: props.user.name }),
|
|
52
|
+
description: t("revokeBody", "They lose the membership benefits right away. A paid subscription is not affected."),
|
|
53
|
+
confirmText: t("revokeConfirm", "Revoke free membership"),
|
|
54
|
+
destructive: true
|
|
55
|
+
});
|
|
56
|
+
if (!confirmed) return;
|
|
57
|
+
await revokeAction.run(async () => {
|
|
58
|
+
await $fetch(base.value, { method: "DELETE" });
|
|
59
|
+
await changed();
|
|
60
|
+
}, { success: t("revoked", "Membership revoked."), error: t("error", "Something went wrong.") });
|
|
61
|
+
}
|
|
62
|
+
async function sendLink() {
|
|
63
|
+
const result = await linkAction.run(() => $fetch(`${base.value}/payment-link`, {
|
|
57
64
|
method: "POST",
|
|
58
65
|
body: { planKey: planKey.value, email: emailUser.value }
|
|
59
|
-
});
|
|
66
|
+
}), { error: t("error", "Something went wrong.") });
|
|
67
|
+
if (!result) return;
|
|
60
68
|
link.value = result.url;
|
|
61
|
-
toast.add({
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await navigator.clipboard.writeText(link.value);
|
|
67
|
-
toast.add({ title: t("copied", "Link copied."), color: "success" });
|
|
68
|
-
} catch (cause) {
|
|
69
|
-
fail(cause);
|
|
70
|
-
}
|
|
69
|
+
toast.add({
|
|
70
|
+
title: result.emailed ? t("linkEmailed", "Payment link created and emailed.") : t("linkCreated", "Payment link created."),
|
|
71
|
+
color: "success",
|
|
72
|
+
icon: "i-lucide-check"
|
|
73
|
+
});
|
|
71
74
|
}
|
|
72
75
|
</script>
|
|
73
76
|
|
|
74
77
|
<template>
|
|
75
|
-
<div class="
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
variant="subtle"
|
|
90
|
-
:color="current.source === 'manual' ? 'info' : 'success'"
|
|
91
|
-
:label="planLabel(current.planKey)"
|
|
92
|
-
/>
|
|
93
|
-
<span class="text-muted">
|
|
94
|
-
{{ current.source === "manual" ? t("free", "Free of charge") : t("paid", "Paid (Stripe)") }}
|
|
95
|
-
·
|
|
96
|
-
{{ current.currentPeriodEnd ? t("until", "until {date}", { date: formatDate(current.currentPeriodEnd) }) : t("openEnded", "No end date") }}
|
|
97
|
-
</span>
|
|
78
|
+
<div class="flex flex-col gap-4">
|
|
79
|
+
<template v-if="current">
|
|
80
|
+
<DescriptionList
|
|
81
|
+
:items="details"
|
|
82
|
+
>
|
|
83
|
+
<template #plan>
|
|
84
|
+
<UBadge
|
|
85
|
+
variant="subtle"
|
|
86
|
+
:color="current.source === 'manual' ? 'info' : statusColor('active')"
|
|
87
|
+
:label="planLabel(current.planKey)"
|
|
88
|
+
/>
|
|
89
|
+
</template>
|
|
90
|
+
</DescriptionList>
|
|
91
|
+
<div v-if="current.source === 'manual'">
|
|
98
92
|
<UButton
|
|
99
|
-
v-if="current.source === 'manual'"
|
|
100
93
|
color="error"
|
|
101
94
|
variant="soft"
|
|
102
95
|
size="sm"
|
|
103
96
|
icon="i-lucide-user-x"
|
|
104
|
-
:loading="busy
|
|
97
|
+
:loading="revokeAction.busy.value"
|
|
105
98
|
:label="t('revoke', 'Revoke free membership')"
|
|
106
99
|
@click="revoke"
|
|
107
100
|
/>
|
|
108
|
-
</
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
103
|
+
<div
|
|
104
|
+
v-else-if="loading"
|
|
105
|
+
class="space-y-2"
|
|
106
|
+
>
|
|
107
|
+
<USkeleton class="h-6 w-1/2" />
|
|
108
|
+
<USkeleton class="h-9 w-2/3" />
|
|
115
109
|
</div>
|
|
110
|
+
<p
|
|
111
|
+
v-else
|
|
112
|
+
class="text-muted text-sm"
|
|
113
|
+
>
|
|
114
|
+
{{ t("none", "No active membership.") }}
|
|
115
|
+
</p>
|
|
116
116
|
|
|
117
117
|
<p
|
|
118
118
|
v-if="!loading && current && current.source !== 'manual'"
|
|
@@ -122,7 +122,7 @@ async function copy() {
|
|
|
122
122
|
</p>
|
|
123
123
|
<div
|
|
124
124
|
v-else-if="!loading && planItems.length"
|
|
125
|
-
class="
|
|
125
|
+
class="flex flex-col gap-3"
|
|
126
126
|
>
|
|
127
127
|
<div class="flex flex-wrap items-end gap-3">
|
|
128
128
|
<UFormField :label="t('plan', 'Plan')">
|
|
@@ -142,7 +142,7 @@ async function copy() {
|
|
|
142
142
|
<UButton
|
|
143
143
|
icon="i-lucide-gift"
|
|
144
144
|
:disabled="!planKey"
|
|
145
|
-
:loading="busy
|
|
145
|
+
:loading="grantAction.busy.value"
|
|
146
146
|
:label="t('grant', 'Grant free')"
|
|
147
147
|
@click="grant"
|
|
148
148
|
/>
|
|
@@ -154,7 +154,7 @@ async function copy() {
|
|
|
154
154
|
variant="outline"
|
|
155
155
|
icon="i-lucide-link"
|
|
156
156
|
:disabled="!planKey"
|
|
157
|
-
:loading="busy
|
|
157
|
+
:loading="linkAction.busy.value"
|
|
158
158
|
:label="t('sendLink', 'Send payment link')"
|
|
159
159
|
@click="sendLink"
|
|
160
160
|
/>
|
|
@@ -164,29 +164,25 @@ async function copy() {
|
|
|
164
164
|
/>
|
|
165
165
|
</div>
|
|
166
166
|
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class="text-muted text-xs"
|
|
187
|
-
>
|
|
188
|
-
{{ t("linkHint", "The link is valid for 24 hours.") }}
|
|
189
|
-
</p>
|
|
167
|
+
<template v-if="link">
|
|
168
|
+
<div class="flex items-center gap-2">
|
|
169
|
+
<UInput
|
|
170
|
+
:model-value="link"
|
|
171
|
+
readonly
|
|
172
|
+
class="flex-1"
|
|
173
|
+
/>
|
|
174
|
+
<UButton
|
|
175
|
+
color="neutral"
|
|
176
|
+
variant="outline"
|
|
177
|
+
icon="i-lucide-copy"
|
|
178
|
+
:label="t('copy', 'Copy link')"
|
|
179
|
+
@click="copy(link, t('copied', 'Link copied.'))"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
<p class="text-muted text-xs">
|
|
183
|
+
{{ t("linkHint", "The link is valid for 24 hours.") }}
|
|
184
|
+
</p>
|
|
185
|
+
</template>
|
|
190
186
|
</div>
|
|
191
187
|
</div>
|
|
192
188
|
</template>
|
|
@@ -5,10 +5,10 @@ type __VLS_Props = {
|
|
|
5
5
|
email: string;
|
|
6
6
|
};
|
|
7
7
|
};
|
|
8
|
-
declare const __VLS_export: import("
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
9
|
refresh: () => any;
|
|
10
|
-
}, string, import("
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
11
11
|
onRefresh?: (() => any) | undefined;
|
|
12
|
-
}>, {}, {}, {}, {}, string, import("
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
13
|
declare const _default: typeof __VLS_export;
|
|
14
14
|
export default _default;
|
|
@@ -10,6 +10,12 @@ export interface UserSubscription {
|
|
|
10
10
|
endedAt: string | null;
|
|
11
11
|
source?: 'stripe' | 'manual';
|
|
12
12
|
}
|
|
13
|
+
type CheckoutResponse = {
|
|
14
|
+
url: string;
|
|
15
|
+
} | {
|
|
16
|
+
updated: true;
|
|
17
|
+
kind: 'upgrade' | 'downgrade' | 'resume';
|
|
18
|
+
};
|
|
13
19
|
export declare const useSubscription: () => {
|
|
14
20
|
subscription: any;
|
|
15
21
|
plan: any;
|
|
@@ -23,6 +29,7 @@ export declare const useSubscription: () => {
|
|
|
23
29
|
subscribe: (planKeyOrPriceId: string, options?: {
|
|
24
30
|
successUrl?: string;
|
|
25
31
|
cancelUrl?: string;
|
|
26
|
-
}) => Promise<
|
|
32
|
+
}) => Promise<CheckoutResponse>;
|
|
27
33
|
openPortal: (returnUrl?: string) => Promise<void>;
|
|
28
34
|
};
|
|
35
|
+
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const __VLS_export: import("
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
2
|
declare const _default: typeof __VLS_export;
|
|
3
3
|
export default _default;
|