@frigear-nu/subscriptions 0.0.10 → 0.1.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.
Files changed (34) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +9 -4
  3. package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +4 -4
  4. package/dist/runtime/app/components/SubscriptionPlans.vue +4 -1
  5. package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +4 -4
  6. package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +7 -2
  7. package/dist/runtime/app/components/SubscriptionStatus.vue +25 -22
  8. package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +7 -2
  9. package/dist/runtime/app/components/Users/Admin/Membership.d.vue.ts +3 -3
  10. package/dist/runtime/app/components/Users/Admin/Membership.vue +90 -94
  11. package/dist/runtime/app/components/Users/Admin/Membership.vue.d.ts +3 -3
  12. package/dist/runtime/app/composables/useSubscription.d.ts +8 -1
  13. package/dist/runtime/app/pages/cancel.d.vue.ts +1 -1
  14. package/dist/runtime/app/pages/cancel.vue +7 -15
  15. package/dist/runtime/app/pages/cancel.vue.d.ts +1 -1
  16. package/dist/runtime/app/pages/membership.d.vue.ts +1 -1
  17. package/dist/runtime/app/pages/membership.vue +21 -23
  18. package/dist/runtime/app/pages/membership.vue.d.ts +1 -1
  19. package/dist/runtime/app/pages/success.d.vue.ts +1 -1
  20. package/dist/runtime/app/pages/success.vue +7 -17
  21. package/dist/runtime/app/pages/success.vue.d.ts +1 -1
  22. package/dist/runtime/app/tabs/Membership.d.vue.ts +3 -3
  23. package/dist/runtime/app/tabs/Membership.vue +26 -21
  24. package/dist/runtime/app/tabs/Membership.vue.d.ts +3 -3
  25. package/dist/runtime/app/widgets/AdminCurrentSubscription.d.vue.ts +1 -1
  26. package/dist/runtime/app/widgets/AdminCurrentSubscription.vue +9 -5
  27. package/dist/runtime/app/widgets/AdminCurrentSubscription.vue.d.ts +1 -1
  28. package/dist/runtime/app/widgets/MembershipSummary.d.vue.ts +3 -0
  29. package/dist/runtime/app/widgets/MembershipSummary.vue +36 -0
  30. package/dist/runtime/app/widgets/MembershipSummary.vue.d.ts +3 -0
  31. package/dist/runtime/i18n/lang/da.json +16 -4
  32. package/dist/runtime/i18n/lang/en.json +16 -4
  33. package/dist/runtime/menus.js +3 -5
  34. package/package.json +18 -18
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/subscriptions",
3
3
  "configKey": "subscriptions",
4
- "version": "0.0.10",
4
+ "version": "0.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -22,11 +22,13 @@ const module$1 = defineNuxtModule({
22
22
  "@frigear-nu/mcp": { version: ">=0.0.1" },
23
23
  "@frigear-nu/users": { version: ">=0.0.1" },
24
24
  "@frigear-nu/stripe": { version: ">=0.0.1" },
25
+ // The pages and admin pieces are built from its components and composables (`SectionCard`, `useFormat`, ...).
26
+ "@frigear-nu/ui": { version: ">=0.0.1" },
25
27
  "@frigear-nu/menu": { version: ">=0.0.1", optional: true },
26
28
  // Membership invoices are only announced (and stored as payments) when it is installed.
27
29
  "@frigear-nu/payments": { version: ">=0.0.0", optional: true },
28
30
  "@frigear-nu/widget": { version: ">=0.0.0", optional: true },
29
- // The admin user tab and card are built from its components, so they are only added with it.
31
+ // The admin user tab and card live on its admin user page, so they are only added with it.
30
32
  "@frigear-nu/admin": { version: ">=0.0.0", optional: true }
31
33
  },
32
34
  async setup(options, nuxt) {
@@ -63,8 +65,7 @@ const module$1 = defineNuxtModule({
63
65
  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
66
  allowChangeInPortal: settings.allowChangeInPortal === true
65
67
  };
66
- await registerDatabaseTable(nuxt, {
67
- name: "subscriptions",
68
+ registerDatabaseTable(nuxt, {
68
69
  resolver,
69
70
  schema: (dialect) => `./runtime/server/db/schema.${dialect}`
70
71
  });
@@ -145,7 +146,11 @@ const module$1 = defineNuxtModule({
145
146
  { name: "subscriptions-success", path: successPath, file: "./runtime/app/pages/success.vue" },
146
147
  { name: "subscriptions-cancel", path: cancelPath, file: "./runtime/app/pages/cancel.vue" }
147
148
  ]);
148
- addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
149
+ addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "account" });
150
+ if (membershipPath && hasNuxtModule("@frigear-nu/widget", nuxt)) {
151
+ const { extendWidgets } = await import('@frigear-nu/widget/utils');
152
+ extendWidgets(nuxt, "account.overview", { id: "subscriptions-membership", component: resolver.resolve("./runtime/app/widgets/MembershipSummary.vue"), order: 10 });
153
+ }
149
154
  }
150
155
  });
151
156
 
@@ -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): string;
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("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
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("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
65
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
66
66
  onSelect?: ((plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any) | undefined;
67
- }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
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
- return formatMoney(price.amount, props.currency ?? price.currency, props.locale);
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): string;
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("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
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("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
65
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
66
66
  onSelect?: ((plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any) | undefined;
67
- }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
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<any>;
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("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
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: "success", icon: "i-lucide-shield-check" };
20
- if (value === "trialing") return { label: t("trialing", "Trial"), color: "info" };
21
- if (value === "past_due" || value === "unpaid") return { label: t("pastDue", "Past due"), color: "warning" };
22
- if (value === "canceled") return { label: t("canceled", "Canceled"), color: "neutral" };
23
- return { label: t("other", "Inactive"), color: "neutral" };
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
- function formatDate(value) {
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
- <UCard :ui="{ root: 'rounded-xl' }">
41
- <template #header>
42
- <slot name="header">
43
- <h3 class="font-semibold">
44
- {{ t("title", "Your subscription") }}
45
- </h3>
46
- </slot>
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-sm text-muted">
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-sm text-muted">{{ t("currentPlan", "Current plan") }}</span>
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="soft"
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="rounded-md bg-muted/50 px-4 py-3 text-sm"
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-sm text-muted">
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
- </UCard>
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<any>;
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("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
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("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
9
  refresh: () => any;
10
- }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
11
  onRefresh?: (() => any) | undefined;
12
- }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
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 formatDate = (value) => new Date(value).toLocaleDateString();
24
- function fail(cause) {
25
- toast.add({ title: cause.statusMessage ?? t("error", "Something went wrong."), color: "error" });
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
- async function run(kind, action) {
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
- const revoke = () => run("revoke", async () => {
51
- await $fetch(base.value, { method: "DELETE" });
52
- toast.add({ title: t("revoked", "Membership revoked."), color: "success" });
53
- await changed();
54
- });
55
- const sendLink = () => run("link", async () => {
56
- const result = await $fetch(`${base.value}/payment-link`, {
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({ title: result.emailed ? t("linkEmailed", "Payment link created and emailed.") : t("linkCreated", "Payment link created."), color: "success" });
62
- });
63
- async function copy() {
64
- if (!link.value) return;
65
- try {
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="space-y-4">
76
- <div
77
- v-if="loading"
78
- class="space-y-2"
79
- >
80
- <USkeleton class="h-6 w-1/2" />
81
- <USkeleton class="h-9 w-2/3" />
82
- </div>
83
- <div
84
- v-else
85
- class="flex flex-wrap items-center gap-2 text-sm"
86
- >
87
- <template v-if="current">
88
- <UBadge
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 === 'revoke'"
97
+ :loading="revokeAction.busy.value"
105
98
  :label="t('revoke', 'Revoke free membership')"
106
99
  @click="revoke"
107
100
  />
108
- </template>
109
- <p
110
- v-else
111
- class="text-muted"
112
- >
113
- {{ t("none", "No active membership.") }}
114
- </p>
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="space-y-3"
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 === 'grant'"
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 === 'link'"
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
- <div
168
- v-if="link"
169
- class="flex items-center gap-2"
170
- >
171
- <UInput
172
- :model-value="link"
173
- readonly
174
- class="flex-1"
175
- />
176
- <UButton
177
- color="neutral"
178
- variant="soft"
179
- icon="i-lucide-copy"
180
- :label="t('copy', 'Copy link')"
181
- @click="copy"
182
- />
183
- </div>
184
- <p
185
- v-if="link"
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("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
9
  refresh: () => any;
10
- }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
11
  onRefresh?: (() => any) | undefined;
12
- }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
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<any>;
32
+ }) => Promise<CheckoutResponse>;
27
33
  openPortal: (returnUrl?: string) => Promise<void>;
28
34
  };
35
+ export {};
@@ -1,3 +1,3 @@
1
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
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;
@@ -1,23 +1,15 @@
1
1
  <script setup>
2
2
  defineOptions({ name: "SubscriptionCancelPage" });
3
3
  const t = useModuleT("subscriptions.cancel");
4
+ const { localize } = useLocaleLink();
4
5
  const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
5
6
  </script>
6
7
 
7
8
  <template>
8
- <UContainer>
9
- <UPage>
10
- <UPageBody class="space-y-6">
11
- <UPageHeader
12
- :title="t('title', 'Checkout canceled')"
13
- :description="t('body', 'No changes were made and you have not been charged.')"
14
- />
15
- <UButton
16
- v-if="membership"
17
- :to="membership"
18
- :label="t('back', 'Back to plans')"
19
- />
20
- </UPageBody>
21
- </UPage>
22
- </UContainer>
9
+ <UEmpty
10
+ icon="i-lucide-circle-x"
11
+ :title="t('title', 'Checkout canceled')"
12
+ :description="t('body', 'No changes were made and you have not been charged.')"
13
+ :actions="membership ? [{ label: t('back', 'Back to membership'), to: localize(membership) }] : []"
14
+ />
23
15
  </template>
@@ -1,3 +1,3 @@
1
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
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;
@@ -1,3 +1,3 @@
1
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
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;
@@ -9,27 +9,25 @@ const { loggedIn } = useUserSession();
9
9
  </script>
10
10
 
11
11
  <template>
12
- <UContainer>
13
- <UPage>
14
- <UPageBody class="space-y-8">
15
- <SubscriptionStatus v-if="isActive" />
16
- <div
17
- v-else-if="loggedIn"
18
- class="space-y-2 text-center"
19
- >
20
- <h1 class="text-3xl font-bold tracking-tight sm:text-4xl">
21
- {{ t("status.heroTitle", "Join the team today") }}
22
- </h1>
23
- <p class="text-muted">
24
- {{ t("status.heroDescription", "Set up your membership to get started!") }}
25
- </p>
26
- </div>
27
- <UPageHeader
28
- v-if="cancelsAt"
29
- :title="t('plans.resubscribe', 'Resubscribe')"
30
- />
31
- <SubscriptionPlans confirm-change />
32
- </UPageBody>
33
- </UPage>
34
- </UContainer>
12
+ <div class="flex flex-col gap-6">
13
+ <SubscriptionStatus v-if="isActive" />
14
+ <div
15
+ v-else-if="loggedIn"
16
+ class="flex flex-col gap-1"
17
+ >
18
+ <h2 class="text-highlighted text-lg font-semibold">
19
+ {{ t("status.heroTitle", "Join the team today") }}
20
+ </h2>
21
+ <p class="text-muted text-sm">
22
+ {{ t("status.heroDescription", "Set up your membership to get started!") }}
23
+ </p>
24
+ </div>
25
+ <h2
26
+ v-if="cancelsAt"
27
+ class="text-highlighted text-lg font-semibold"
28
+ >
29
+ {{ t("plans.resubscribe", "Resubscribe") }}
30
+ </h2>
31
+ <SubscriptionPlans confirm-change />
32
+ </div>
35
33
  </template>
@@ -1,3 +1,3 @@
1
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
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;
@@ -1,3 +1,3 @@
1
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
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;
@@ -1,9 +1,9 @@
1
1
  <script setup>
2
- import SubscriptionStatus from "../components/SubscriptionStatus.vue";
3
2
  import { useSubscription } from "../composables/useSubscription";
4
3
  defineOptions({ name: "SubscriptionSuccessPage" });
5
4
  const t = useModuleT("subscriptions.success");
6
5
  const { refresh, isActive } = useSubscription();
6
+ const { localize } = useLocaleLink();
7
7
  const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
8
8
  const MAX_ATTEMPTS = 40;
9
9
  const INTERVAL_MS = 3e3;
@@ -20,20 +20,10 @@ onMounted(async () => {
20
20
  </script>
21
21
 
22
22
  <template>
23
- <UContainer>
24
- <UPage>
25
- <UPageBody class="space-y-6">
26
- <UPageHeader
27
- :title="t('title', 'Thank you!')"
28
- :description="isActive ? t('activeBody', 'Your subscription is now active.') : t('body', 'Your subscription is being activated. This can take a few seconds.')"
29
- />
30
- <SubscriptionStatus />
31
- <UButton
32
- v-if="membership"
33
- :to="membership"
34
- :label="t('continue', 'Continue')"
35
- />
36
- </UPageBody>
37
- </UPage>
38
- </UContainer>
23
+ <UEmpty
24
+ :icon="isActive ? 'i-lucide-circle-check' : 'i-lucide-loader-circle'"
25
+ :title="t('title', 'Thank you!')"
26
+ :description="isActive ? t('activeBody', 'Your subscription is now active.') : t('body', 'Your subscription is being activated. This can take a few seconds.')"
27
+ :actions="membership ? [{ label: t('back', 'Back to membership'), to: localize(membership) }] : []"
28
+ />
39
29
  </template>
@@ -1,3 +1,3 @@
1
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
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;
@@ -5,10 +5,10 @@ type __VLS_Props = {
5
5
  email: string;
6
6
  };
7
7
  };
8
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
9
  refresh: () => any;
10
- }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
11
  onRefresh?: (() => any) | undefined;
12
- }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
13
  declare const _default: typeof __VLS_export;
14
14
  export default _default;
@@ -5,14 +5,16 @@ const props = defineProps({
5
5
  });
6
6
  const emit = defineEmits(["refresh"]);
7
7
  const t = useModuleT("subscriptions.admin");
8
+ const format = useFormat();
9
+ const UBadge = resolveComponent("UBadge");
8
10
  const { data: history, refresh: refreshHistory, status } = useLazyFetch(
9
11
  () => `/api/admin/users/${props.user.id}/subscriptions`,
10
12
  { key: () => `admin-subscriptions-${props.user.id}` }
11
13
  );
12
- const date = (value) => value ? new Date(value).toLocaleDateString() : "\u2013";
14
+ const date = (value) => format.date(value) || "\u2013";
13
15
  const columns = computed(() => [
14
16
  { accessorKey: "planKey", header: t("history.plan", "Plan"), cell: ({ row }) => row.original.planKey ?? row.original.priceId },
15
- { accessorKey: "status", header: t("history.status", "Status") },
17
+ { accessorKey: "status", header: t("history.status", "Status"), cell: ({ row }) => h(UBadge, { label: row.original.status, variant: "subtle", color: statusColor(row.original.status) }) },
16
18
  { accessorKey: "source", header: t("history.source", "Source"), cell: ({ row }) => row.original.source === "manual" ? t("free", "Free of charge") : t("paid", "Paid (Stripe)") },
17
19
  { accessorKey: "currentPeriodStart", header: t("history.started", "Started"), cell: ({ row }) => date(row.original.currentPeriodStart) },
18
20
  { accessorKey: "currentPeriodEnd", header: t("history.periodEnd", "Period end"), cell: ({ row }) => date(row.original.currentPeriodEnd) },
@@ -25,29 +27,32 @@ async function changed() {
25
27
  </script>
26
28
 
27
29
  <template>
28
- <div class="flex flex-col gap-6">
29
- <AdminSection :title="t('membership', 'Membership')">
30
+ <div class="flex flex-col gap-4">
31
+ <SectionCard :title="t('membership', 'Membership')">
30
32
  <UsersAdminMembership
31
33
  :user="user"
32
34
  @refresh="changed"
33
35
  />
34
- </AdminSection>
36
+ </SectionCard>
35
37
 
36
- <AdminSection :title="t('history.title', 'Subscription history')">
37
- <template #flush>
38
- <UTable
39
- :data="history?.subscriptions ?? []"
40
- :columns="columns"
41
- :loading="status === 'pending'"
42
- >
43
- <template #empty>
44
- <AdminEmpty
45
- icon="i-lucide-badge-check"
46
- :title="t('history.empty', 'No subscriptions yet.')"
47
- />
48
- </template>
49
- </UTable>
50
- </template>
51
- </AdminSection>
38
+ <SectionCard
39
+ :title="t('history.title', 'Subscription history')"
40
+ flush
41
+ >
42
+ <UTable
43
+ :data="history?.subscriptions ?? []"
44
+ :columns="columns"
45
+ :loading="status === 'pending'"
46
+ :ui="tableUi({ tr: '' })"
47
+ >
48
+ <template #empty>
49
+ <UEmpty
50
+ variant="naked"
51
+ icon="i-lucide-badge-check"
52
+ :title="t('history.empty', 'No subscriptions yet.')"
53
+ />
54
+ </template>
55
+ </UTable>
56
+ </SectionCard>
52
57
  </div>
53
58
  </template>
@@ -5,10 +5,10 @@ type __VLS_Props = {
5
5
  email: string;
6
6
  };
7
7
  };
8
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
9
  refresh: () => any;
10
- }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
11
  onRefresh?: (() => any) | undefined;
12
- }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
13
  declare const _default: typeof __VLS_export;
14
14
  export default _default;
@@ -6,6 +6,6 @@ type __VLS_Props = {
6
6
  } | null;
7
7
  };
8
8
  };
9
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
9
+ declare const __VLS_export: 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>;
10
10
  declare const _default: typeof __VLS_export;
11
11
  export default _default;
@@ -7,17 +7,21 @@ const t = useModuleT("subscriptions.admin");
7
7
  </script>
8
8
 
9
9
  <template>
10
- <AdminStatCard
10
+ <StatCard
11
11
  :label="t('currentSubscription', 'Current subscription')"
12
+ icon="i-lucide-badge-check"
12
13
  :empty="t('none', 'No active membership.')"
13
14
  >
14
- <template v-if="user.subscription">
15
+ <template
16
+ v-if="user.subscription"
17
+ #default
18
+ >
15
19
  <UBadge
16
20
  variant="subtle"
17
- :color="['active', 'trialing'].includes(user.subscription.status) ? 'success' : 'neutral'"
21
+ :color="statusColor(user.subscription.status)"
18
22
  :label="user.subscription.planKey ?? user.subscription.status"
19
23
  />
20
- <span class="text-muted">{{ user.subscription.status }}</span>
24
+ <span class="text-muted text-sm font-normal">{{ user.subscription.status }}</span>
21
25
  </template>
22
- </AdminStatCard>
26
+ </StatCard>
23
27
  </template>
@@ -6,6 +6,6 @@ type __VLS_Props = {
6
6
  } | null;
7
7
  };
8
8
  };
9
- declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
9
+ declare const __VLS_export: 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>;
10
10
  declare const _default: typeof __VLS_export;
11
11
  export default _default;
@@ -0,0 +1,3 @@
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
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,36 @@
1
+ <script setup>
2
+ import { usePlans } from "../composables/usePlans";
3
+ import { useSubscription } from "../composables/useSubscription";
4
+ defineOptions({ name: "SubscriptionsMembershipSummary" });
5
+ const t = useModuleT("subscriptions.overview");
6
+ const ts = useModuleT("subscriptions.status");
7
+ const tp = useModuleT("subscriptions.catalog");
8
+ const format = useFormat();
9
+ const { localize } = useLocaleLink();
10
+ const { plans } = usePlans();
11
+ const { subscription, plan, status, isActive, cancelsAt } = useSubscription();
12
+ const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
13
+ const planLabel = computed(() => {
14
+ if (!plan.value) return "";
15
+ return tp(`${plan.value}.label`, plans.value.find((p) => p.key === plan.value)?.label ?? plan.value);
16
+ });
17
+ const hint = computed(() => {
18
+ const end = subscription.value?.currentPeriodEnd;
19
+ if (!end) return void 0;
20
+ const date = format.date(end);
21
+ if (subscription.value?.source === "manual") return ts("freeUntil", "Free membership until {date}", { date });
22
+ return cancelsAt.value ? ts("cancels", "Ends on {date}", { date }) : ts("renews", "Renews on {date}", { date });
23
+ });
24
+ </script>
25
+
26
+ <template>
27
+ <StatCard
28
+ :label="t('label', 'Membership')"
29
+ icon="i-lucide-credit-card"
30
+ :to="membership ? localize(membership) : void 0"
31
+ :loading="status === 'pending' && !subscription"
32
+ :value="isActive ? planLabel : void 0"
33
+ :hint="isActive ? hint : void 0"
34
+ :empty="t('none', 'No active membership')"
35
+ />
36
+ </template>
@@ -0,0 +1,3 @@
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
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -20,7 +20,8 @@
20
20
  "confirmDowngrade": "Hvis du nedgraderer, kører dit nuværende abonnement til næste forfaldsdato, hvorefter det skifter.",
21
21
  "subscribeTo": "Du abonnerer nu på:",
22
22
  "changeTitle": "Opdatér abonnement",
23
- "checkoutTitle": "Bliv medlem"
23
+ "checkoutTitle": "Bliv medlem",
24
+ "confirmResume": "Dit medlemskab fornyes som hidtil."
24
25
  },
25
26
  "interval": {
26
27
  "day": "dag",
@@ -61,12 +62,12 @@
61
62
  "title": "Tak!",
62
63
  "body": "Dit abonnement bliver aktiveret. Det kan tage et par sekunder.",
63
64
  "activeBody": "Dit abonnement er aktiveret.",
64
- "continue": "Fortsæt"
65
+ "back": "Tilbage til medlemskab"
65
66
  },
66
67
  "cancel": {
67
68
  "title": "Betaling annulleret",
68
69
  "body": "Der blev ikke foretaget ændringer, og du er ikke blevet opkrævet.",
69
- "back": "Tilbage til planer"
70
+ "back": "Tilbage til medlemskab"
70
71
  },
71
72
  "errors": {
72
73
  "checkout": "Kunne ikke starte betalingen.",
@@ -113,7 +114,18 @@
113
114
  "started": "Startet",
114
115
  "periodEnd": "Periodeslut",
115
116
  "ended": "Afsluttet"
116
- }
117
+ },
118
+ "type": "Type",
119
+ "validUntil": "Gyldigt til",
120
+ "status": "Status",
121
+ "revokeTitle": "Fjern det gratis medlemskab for {name}?",
122
+ "revokeBody": "Brugeren mister medlemsfordelene med det samme. Et betalt abonnement påvirkes ikke.",
123
+ "revokeConfirm": "Fjern gratis medlemskab"
124
+ },
125
+ "overview": {
126
+ "label": "Medlemskab",
127
+ "none": "Intet aktivt medlemskab",
128
+ "manage": "Administrér medlemskab"
117
129
  }
118
130
  }
119
131
  }
@@ -20,7 +20,8 @@
20
20
  "confirmDowngrade": "If you downgrade, your current subscription runs until the next due date, then switches.",
21
21
  "subscribeTo": "You will now subscribe to:",
22
22
  "changeTitle": "Update subscription",
23
- "checkoutTitle": "Get a membership"
23
+ "checkoutTitle": "Get a membership",
24
+ "confirmResume": "Your membership will keep renewing as before."
24
25
  },
25
26
  "interval": {
26
27
  "day": "day",
@@ -61,12 +62,12 @@
61
62
  "title": "Thank you!",
62
63
  "body": "Your subscription is being activated. This can take a few seconds.",
63
64
  "activeBody": "Your subscription is now active.",
64
- "continue": "Continue"
65
+ "back": "Back to membership"
65
66
  },
66
67
  "cancel": {
67
68
  "title": "Checkout canceled",
68
69
  "body": "No changes were made and you have not been charged.",
69
- "back": "Back to plans"
70
+ "back": "Back to membership"
70
71
  },
71
72
  "errors": {
72
73
  "checkout": "Failed to start checkout.",
@@ -113,7 +114,18 @@
113
114
  "started": "Started",
114
115
  "periodEnd": "Period end",
115
116
  "ended": "Ended"
116
- }
117
+ },
118
+ "type": "Type",
119
+ "validUntil": "Valid until",
120
+ "status": "Status",
121
+ "revokeTitle": "Revoke the free membership of {name}?",
122
+ "revokeBody": "They lose the membership benefits right away. A paid subscription is not affected.",
123
+ "revokeConfirm": "Revoke free membership"
124
+ },
125
+ "overview": {
126
+ "label": "Membership",
127
+ "none": "No active membership",
128
+ "manage": "Manage membership"
117
129
  }
118
130
  }
119
131
  }
@@ -2,7 +2,7 @@ import { defineMenuItem, defineMenuSource, extendMenuItem } from "@frigear-nu/me
2
2
  export default defineMenuSource(() => {
3
3
  const config = useRuntimeConfig().public.subscriptions;
4
4
  if (config?.pages?.membership) {
5
- const item = defineMenuItem({
5
+ extendMenuItem("account", defineMenuItem({
6
6
  id: "subscription",
7
7
  label: "Subscription",
8
8
  labelKey: "subscriptions.menu.subscription.label",
@@ -10,9 +10,7 @@ export default defineMenuSource(() => {
10
10
  descriptionKey: "subscriptions.menu.subscription.description",
11
11
  icon: "i-lucide-credit-card",
12
12
  to: config.pages.membership,
13
- order: 30
14
- });
15
- extendMenuItem("account", item);
16
- extendMenuItem("dashboard.account", item);
13
+ order: 15
14
+ }));
17
15
  }
18
16
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/subscriptions",
3
- "version": "0.0.10",
3
+ "version": "0.1.0",
4
4
  "description": "Recurring subscriptions (plans, checkout, upgrade/downgrade, billing portal and entitlements) for the frigear ecosystem, built on @frigear-nu/stripe.",
5
5
  "homepage": "https://docs.frigear.dev/modules/subscriptions",
6
6
  "repository": {
@@ -30,14 +30,14 @@
30
30
  "playground"
31
31
  ],
32
32
  "dependencies": {
33
- "@frigear-nu/auth": "0.0.5",
34
- "@frigear-nu/mcp": "0.0.5",
35
- "@frigear-nu/module-kit": "1.0.1",
36
- "@frigear-nu/stripe": "0.0.10",
37
- "@frigear-nu/ui": "0.0.9",
38
- "@frigear-nu/users": "1.0.11",
33
+ "@frigear-nu/auth": "0.1.0",
34
+ "@frigear-nu/mcp": "0.1.0",
35
+ "@frigear-nu/module-kit": "1.1.0",
36
+ "@frigear-nu/stripe": "0.1.0",
37
+ "@frigear-nu/ui": "0.1.0",
38
+ "@frigear-nu/users": "1.1.0",
39
39
  "@libsql/client": "^0.18.0",
40
- "@nuxt/kit": "^4.6.0",
40
+ "@nuxt/kit": "^4.6.1",
41
41
  "@vueuse/core": "^15.0.0",
42
42
  "drizzle-kit": "^0.31.11",
43
43
  "drizzle-orm": "^0.45.3",
@@ -47,9 +47,9 @@
47
47
  "zod": "^4.6.5"
48
48
  },
49
49
  "peerDependencies": {
50
- "@frigear-nu/menu": "0.0.11",
51
- "@frigear-nu/widget": "0.0.4",
52
- "@nuxt/ui": "^4.10.0",
50
+ "@frigear-nu/menu": "0.1.0",
51
+ "@frigear-nu/widget": "0.1.0",
52
+ "@nuxt/ui": "^4.11.2",
53
53
  "@nuxthub/core": "^0.10.8"
54
54
  },
55
55
  "peerDependenciesMeta": {
@@ -64,21 +64,21 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
- "@frigear-nu/menu": "0.0.11",
68
- "@frigear-nu/widget": "0.0.4",
67
+ "@frigear-nu/menu": "0.1.0",
68
+ "@frigear-nu/widget": "0.1.0",
69
69
  "@nuxt/devtools": "^3.4.2",
70
70
  "@nuxt/eslint-config": "^1.17.0",
71
71
  "@nuxt/module-builder": "^1.0.3",
72
- "@nuxt/schema": "^4.6.0",
72
+ "@nuxt/schema": "^4.6.1",
73
73
  "@nuxt/test-utils": "^4.3.2",
74
- "@nuxt/ui": "^4.11.0",
74
+ "@nuxt/ui": "^4.11.2",
75
75
  "@nuxthub/core": "^0.10.8",
76
- "@types/node": "latest",
76
+ "@types/node": "^26.6.5",
77
77
  "changelogen": "^0.6.2",
78
- "eslint": "^10.10.0",
78
+ "eslint": "^10.12.0",
79
79
  "nuxt": "^4.6.1",
80
80
  "typescript": "^6.0.3",
81
- "vitest": "^5.0.1",
81
+ "vitest": "^5.0.2",
82
82
  "vue-tsc": "^3.3.11"
83
83
  },
84
84
  "scripts": {