@frigear-nu/subscriptions 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/module.d.mts +65 -2
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +60 -29
  4. package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +31 -27
  5. package/dist/runtime/app/components/SubscriptionPlans.vue +203 -105
  6. package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +31 -27
  7. package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +1 -1
  8. package/dist/runtime/app/components/SubscriptionStatus.vue +45 -24
  9. package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +1 -1
  10. package/dist/runtime/app/components/Users/Admin/Membership.d.vue.ts +14 -0
  11. package/dist/runtime/app/components/Users/Admin/Membership.vue +192 -0
  12. package/dist/runtime/app/components/Users/Admin/Membership.vue.d.ts +14 -0
  13. package/dist/runtime/app/composables/usePlans.d.ts +11 -0
  14. package/dist/runtime/app/composables/usePlans.js +9 -1
  15. package/dist/runtime/app/composables/useSubscription.d.ts +1 -0
  16. package/dist/runtime/app/composables/useSubscription.js +17 -3
  17. package/dist/runtime/app/pages/cancel.d.vue.ts +1 -1
  18. package/dist/runtime/app/pages/cancel.vue.d.ts +1 -1
  19. package/dist/runtime/app/pages/membership.d.vue.ts +1 -1
  20. package/dist/runtime/app/pages/membership.vue +18 -1
  21. package/dist/runtime/app/pages/membership.vue.d.ts +1 -1
  22. package/dist/runtime/app/pages/success.d.vue.ts +1 -1
  23. package/dist/runtime/app/pages/success.vue +9 -3
  24. package/dist/runtime/app/pages/success.vue.d.ts +1 -1
  25. package/dist/runtime/app/plugins/warm-plans.client.d.ts +6 -0
  26. package/dist/runtime/app/plugins/warm-plans.client.js +12 -0
  27. package/dist/runtime/app/tabs/Membership.d.vue.ts +14 -0
  28. package/dist/runtime/app/tabs/Membership.vue +53 -0
  29. package/dist/runtime/app/tabs/Membership.vue.d.ts +14 -0
  30. package/dist/runtime/app/widgets/AdminCurrentSubscription.d.vue.ts +11 -0
  31. package/dist/runtime/app/widgets/AdminCurrentSubscription.vue +23 -0
  32. package/dist/runtime/app/widgets/AdminCurrentSubscription.vue.d.ts +11 -0
  33. package/dist/runtime/i18n/lang/da.json +67 -3
  34. package/dist/runtime/i18n/lang/en.json +67 -3
  35. package/dist/runtime/menus.js +2 -0
  36. package/dist/runtime/server/db/schema.sqlite.d.ts +19 -0
  37. package/dist/runtime/server/db/schema.sqlite.js +2 -0
  38. package/dist/runtime/server/mcp/tools/my-subscriptions.d.ts +2 -0
  39. package/dist/runtime/server/mcp/tools/my-subscriptions.js +9 -0
  40. package/dist/runtime/server/routes/admin/payment-link.post.d.ts +2 -0
  41. package/dist/runtime/server/routes/admin/payment-link.post.js +46 -0
  42. package/dist/runtime/server/routes/admin/subscription.delete.d.ts +2 -0
  43. package/dist/runtime/server/routes/admin/subscription.delete.js +14 -0
  44. package/dist/runtime/server/routes/admin/subscription.get.d.ts +2 -0
  45. package/dist/runtime/server/routes/admin/subscription.get.js +17 -0
  46. package/dist/runtime/server/routes/admin/subscription.post.d.ts +2 -0
  47. package/dist/runtime/server/routes/admin/subscription.post.js +44 -0
  48. package/dist/runtime/server/routes/admin/subscriptions.get.d.ts +2 -0
  49. package/dist/runtime/server/routes/admin/subscriptions.get.js +4 -0
  50. package/dist/runtime/server/routes/subscriptions/checkout.post.js +16 -30
  51. package/dist/runtime/server/routes/subscriptions/me.get.js +2 -20
  52. package/dist/runtime/server/routes/subscriptions/portal.post.js +2 -2
  53. package/dist/runtime/server/stripe/invoice-events.d.ts +2 -0
  54. package/dist/runtime/server/stripe/invoice-events.js +42 -0
  55. package/dist/runtime/server/utils/admin-membership.d.ts +5 -0
  56. package/dist/runtime/server/utils/admin-membership.js +7 -0
  57. package/dist/runtime/server/utils/checkout.d.ts +15 -0
  58. package/dist/runtime/server/utils/checkout.js +22 -0
  59. package/dist/runtime/server/utils/plans.js +20 -1
  60. package/dist/runtime/server/utils/subscriptions.d.ts +3 -0
  61. package/dist/runtime/server/utils/subscriptions.js +22 -3
  62. package/dist/runtime/utils/index.d.ts +1 -1
  63. package/dist/runtime/utils/index.js +1 -1
  64. package/dist/runtime/utils/plans.d.ts +15 -6
  65. package/dist/runtime/utils/plans.js +16 -11
  66. package/dist/types.d.mts +1 -1
  67. package/package.json +18 -5
@@ -0,0 +1,192 @@
1
+ <script setup>
2
+ defineOptions({ name: "UsersAdminMembership" });
3
+ const props = defineProps({
4
+ user: { type: Object, required: true }
5
+ });
6
+ const emit = defineEmits(["refresh"]);
7
+ const t = useModuleT("subscriptions.admin");
8
+ const toast = useToast();
9
+ const base = computed(() => `/api/admin/users/${props.user.id}/subscription`);
10
+ const { data, refresh, status } = useLazyFetch(() => base.value, { key: () => `admin-membership-${props.user.id}` });
11
+ const loading = computed(() => status.value === "pending" && !data.value);
12
+ const planKey = ref(void 0);
13
+ const endsAt = ref("");
14
+ const emailUser = ref(true);
15
+ const link = ref(null);
16
+ const busy = ref(null);
17
+ const planItems = computed(() => (data.value?.plans ?? []).map((plan) => ({ label: plan.label, value: plan.key })));
18
+ watchEffect(() => {
19
+ if (!planKey.value && planItems.value.length) planKey.value = planItems.value[0].value;
20
+ });
21
+ const current = computed(() => data.value?.current ?? null);
22
+ 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
+ }
27
+ async function changed() {
28
+ await refresh();
29
+ emit("refresh");
30
+ }
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 () => {
42
+ await $fetch(base.value, {
43
+ method: "POST",
44
+ body: { planKey: planKey.value, endsAt: endsAt.value ? (/* @__PURE__ */ new Date(`${endsAt.value}T23:59:59`)).toISOString() : null }
45
+ });
46
+ link.value = null;
47
+ toast.add({ title: t("granted", "Membership granted."), color: "success" });
48
+ 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`, {
57
+ method: "POST",
58
+ body: { planKey: planKey.value, email: emailUser.value }
59
+ });
60
+ 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
+ }
71
+ }
72
+ </script>
73
+
74
+ <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>
98
+ <UButton
99
+ v-if="current.source === 'manual'"
100
+ color="error"
101
+ variant="soft"
102
+ size="sm"
103
+ icon="i-lucide-user-x"
104
+ :loading="busy === 'revoke'"
105
+ :label="t('revoke', 'Revoke free membership')"
106
+ @click="revoke"
107
+ />
108
+ </template>
109
+ <p
110
+ v-else
111
+ class="text-muted"
112
+ >
113
+ {{ t("none", "No active membership.") }}
114
+ </p>
115
+ </div>
116
+
117
+ <p
118
+ v-if="!loading && current && current.source !== 'manual'"
119
+ class="text-muted text-sm"
120
+ >
121
+ {{ t("paidHint", "Paid subscriptions are managed in Stripe.") }}
122
+ </p>
123
+ <div
124
+ v-else-if="!loading && planItems.length"
125
+ class="space-y-3"
126
+ >
127
+ <div class="flex flex-wrap items-end gap-3">
128
+ <UFormField :label="t('plan', 'Plan')">
129
+ <USelect
130
+ v-model="planKey"
131
+ :items="planItems"
132
+ :placeholder="t('planPlaceholder', 'Choose a plan')"
133
+ class="min-w-48"
134
+ />
135
+ </UFormField>
136
+ <UFormField :label="t('endsAt', 'Ends on (optional)')">
137
+ <UInput
138
+ v-model="endsAt"
139
+ type="date"
140
+ />
141
+ </UFormField>
142
+ <UButton
143
+ icon="i-lucide-gift"
144
+ :disabled="!planKey"
145
+ :loading="busy === 'grant'"
146
+ :label="t('grant', 'Grant free')"
147
+ @click="grant"
148
+ />
149
+ </div>
150
+
151
+ <div class="flex flex-wrap items-center gap-3">
152
+ <UButton
153
+ color="neutral"
154
+ variant="outline"
155
+ icon="i-lucide-link"
156
+ :disabled="!planKey"
157
+ :loading="busy === 'link'"
158
+ :label="t('sendLink', 'Send payment link')"
159
+ @click="sendLink"
160
+ />
161
+ <UCheckbox
162
+ v-model="emailUser"
163
+ :label="t('emailUser', 'Email the link to the user')"
164
+ />
165
+ </div>
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>
190
+ </div>
191
+ </div>
192
+ </template>
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ user: {
3
+ id: number;
4
+ name: string;
5
+ email: string;
6
+ };
7
+ };
8
+ declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
9
+ refresh: () => any;
10
+ }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
+ onRefresh?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -13,8 +13,19 @@ export interface SubscriptionPlan {
13
13
  label: string;
14
14
  description?: string;
15
15
  rank: number;
16
+ features?: string[];
16
17
  prices: SubscriptionPlanPrice[];
17
18
  }
19
+ export type PlansResponse = {
20
+ plans: SubscriptionPlan[];
21
+ };
22
+ /** Matches the server-side KV cache (`CACHE_TTL_SECONDS`), so a client-side hit is never older than the server's own. */
23
+ export declare const PLANS_CLIENT_CACHE_MS: number;
24
+ export type PlansCache = {
25
+ at: number;
26
+ data: PlansResponse;
27
+ } | null;
28
+ export declare const usePlansCache: () => any;
18
29
  export declare const usePlans: () => {
19
30
  plans: any;
20
31
  status: any;
@@ -1,7 +1,15 @@
1
+ export const PLANS_CLIENT_CACHE_MS = 5 * 60 * 1e3;
2
+ export const usePlansCache = () => useState("subscriptions:plans:cache", () => null);
1
3
  export const usePlans = () => {
4
+ const cache = usePlansCache();
2
5
  const { data, status, error, refresh } = useFetch("/api/subscriptions/plans", {
3
6
  key: "subscriptions:plans",
4
- default: () => ({ plans: [] })
7
+ default: () => ({ plans: [] }),
8
+ getCachedData: (_key, _nuxtApp, ctx) => ctx.cause !== "refresh:manual" && cache.value && Date.now() - cache.value.at < PLANS_CLIENT_CACHE_MS ? cache.value.data : void 0,
9
+ transform: (response) => {
10
+ cache.value = { at: Date.now(), data: response };
11
+ return response;
12
+ }
5
13
  });
6
14
  const plans = computed(
7
15
  () => [...data.value?.plans ?? []].sort((a, b) => a.rank - b.rank)
@@ -8,6 +8,7 @@ export interface UserSubscription {
8
8
  currentPeriodStart: string | null;
9
9
  currentPeriodEnd: string | null;
10
10
  endedAt: string | null;
11
+ source?: 'stripe' | 'manual';
11
12
  }
12
13
  export declare const useSubscription: () => {
13
14
  subscription: any;
@@ -1,22 +1,36 @@
1
1
  const ACTIVE_STATUSES = ["active", "trialing"];
2
+ const CLIENT_CACHE_MS = 60 * 1e3;
3
+ function isEntitled(item) {
4
+ if (!ACTIVE_STATUSES.includes(item.status) || item.endedAt) return false;
5
+ return !(item.source === "manual" && item.currentPeriodEnd && new Date(item.currentPeriodEnd).getTime() <= Date.now());
6
+ }
2
7
  export const useSubscription = () => {
3
8
  const { loggedIn } = useUserSession();
4
9
  const t = useModuleT("subscriptions");
5
10
  const pending = ref(false);
6
11
  const error = ref(null);
12
+ const cache = useState("subscriptions:me:cache", () => null);
7
13
  const { data, status, refresh } = useFetch("/api/subscriptions/me", {
8
14
  key: "subscriptions:me",
9
15
  default: () => ({ subscriptions: [], activePlan: null }),
10
16
  immediate: loggedIn.value,
11
- watch: false
17
+ watch: false,
18
+ getCachedData: (_key, _nuxtApp, ctx) => ctx.cause !== "refresh:manual" && cache.value && Date.now() - cache.value.at < CLIENT_CACHE_MS ? cache.value.data : void 0,
19
+ transform: (response) => {
20
+ cache.value = { at: Date.now(), data: response };
21
+ return response;
22
+ }
12
23
  });
13
24
  watch(loggedIn, (value) => {
14
25
  if (value) refresh();
15
- else data.value = { subscriptions: [], activePlan: null };
26
+ else {
27
+ cache.value = null;
28
+ data.value = { subscriptions: [], activePlan: null };
29
+ }
16
30
  });
17
31
  const subscription = computed(() => {
18
32
  const list = data.value?.subscriptions ?? [];
19
- return list.find((item) => ACTIVE_STATUSES.includes(item.status)) ?? null;
33
+ return list.find(isEntitled) ?? null;
20
34
  });
21
35
  const plan = computed(() => data.value?.activePlan ?? subscription.value?.planKey ?? null);
22
36
  const isActive = computed(() => subscription.value !== null);
@@ -1,3 +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>;
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>;
2
2
  declare const _default: typeof __VLS_export;
3
3
  export default _default;
@@ -1,3 +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>;
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>;
2
2
  declare const _default: typeof __VLS_export;
3
3
  export default _default;
@@ -1,3 +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>;
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>;
2
2
  declare const _default: typeof __VLS_export;
3
3
  export default _default;
@@ -3,7 +3,9 @@ import SubscriptionPlans from "../components/SubscriptionPlans.vue";
3
3
  import SubscriptionStatus from "../components/SubscriptionStatus.vue";
4
4
  import { useSubscription } from "../composables/useSubscription";
5
5
  defineOptions({ name: "SubscriptionMembershipPage" });
6
- const { isActive } = useSubscription();
6
+ const t = useModuleT("subscriptions");
7
+ const { isActive, cancelsAt } = useSubscription();
8
+ const { loggedIn } = useUserSession();
7
9
  </script>
8
10
 
9
11
  <template>
@@ -11,6 +13,21 @@ const { isActive } = useSubscription();
11
13
  <UPage>
12
14
  <UPageBody class="space-y-8">
13
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
+ />
14
31
  <SubscriptionPlans confirm-change />
15
32
  </UPageBody>
16
33
  </UPage>
@@ -1,3 +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>;
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>;
2
2
  declare const _default: typeof __VLS_export;
3
3
  export default _default;
@@ -1,3 +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>;
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>;
2
2
  declare const _default: typeof __VLS_export;
3
3
  export default _default;
@@ -5,10 +5,16 @@ defineOptions({ name: "SubscriptionSuccessPage" });
5
5
  const t = useModuleT("subscriptions.success");
6
6
  const { refresh, isActive } = useSubscription();
7
7
  const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
8
+ const MAX_ATTEMPTS = 40;
9
+ const INTERVAL_MS = 3e3;
10
+ let stopped = false;
11
+ onBeforeUnmount(() => {
12
+ stopped = true;
13
+ });
8
14
  onMounted(async () => {
9
- for (let attempt = 0; attempt < 5 && !isActive.value; attempt++) {
15
+ for (let attempt = 0; attempt < MAX_ATTEMPTS && !stopped && !isActive.value; attempt++) {
10
16
  await refresh();
11
- if (!isActive.value) await new Promise((resolve) => setTimeout(resolve, 2e3));
17
+ if (!isActive.value && !stopped) await new Promise((resolve) => setTimeout(resolve, INTERVAL_MS));
12
18
  }
13
19
  });
14
20
  </script>
@@ -19,7 +25,7 @@ onMounted(async () => {
19
25
  <UPageBody class="space-y-6">
20
26
  <UPageHeader
21
27
  :title="t('title', 'Thank you!')"
22
- :description="t('body', 'Your subscription is being activated. This can take a few seconds.')"
28
+ :description="isActive ? t('activeBody', 'Your subscription is now active.') : t('body', 'Your subscription is being activated. This can take a few seconds.')"
23
29
  />
24
30
  <SubscriptionStatus />
25
31
  <UButton
@@ -1,3 +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>;
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>;
2
2
  declare const _default: typeof __VLS_export;
3
3
  export default _default;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pre-loads the plans once the app is interactive, so the first client-side navigation to a page with the pricing
3
+ * table (from a page that never fetched them) renders instantly instead of waiting for a round trip.
4
+ */
5
+ declare const _default: any;
6
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { PLANS_CLIENT_CACHE_MS, usePlansCache } from "../composables/usePlans.js";
2
+ export default defineNuxtPlugin((nuxtApp) => {
3
+ nuxtApp.hook("app:mounted", () => {
4
+ const cache = usePlansCache();
5
+ if (cache.value && Date.now() - cache.value.at < PLANS_CLIENT_CACHE_MS) return;
6
+ const warm = () => $fetch("/api/subscriptions/plans").then((data) => {
7
+ cache.value = { at: Date.now(), data };
8
+ }).catch(() => void 0);
9
+ if ("requestIdleCallback" in window) window.requestIdleCallback(() => void warm());
10
+ else setTimeout(() => void warm(), 1e3);
11
+ });
12
+ });
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ user: {
3
+ id: number;
4
+ name: string;
5
+ email: string;
6
+ };
7
+ };
8
+ declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
9
+ refresh: () => any;
10
+ }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
+ onRefresh?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,53 @@
1
+ <script setup>
2
+ defineOptions({ name: "UsersAdminMembershipTab" });
3
+ const props = defineProps({
4
+ user: { type: Object, required: true }
5
+ });
6
+ const emit = defineEmits(["refresh"]);
7
+ const t = useModuleT("subscriptions.admin");
8
+ const { data: history, refresh: refreshHistory, status } = useLazyFetch(
9
+ () => `/api/admin/users/${props.user.id}/subscriptions`,
10
+ { key: () => `admin-subscriptions-${props.user.id}` }
11
+ );
12
+ const date = (value) => value ? new Date(value).toLocaleDateString() : "\u2013";
13
+ const columns = computed(() => [
14
+ { accessorKey: "planKey", header: t("history.plan", "Plan"), cell: ({ row }) => row.original.planKey ?? row.original.priceId },
15
+ { accessorKey: "status", header: t("history.status", "Status") },
16
+ { accessorKey: "source", header: t("history.source", "Source"), cell: ({ row }) => row.original.source === "manual" ? t("free", "Free of charge") : t("paid", "Paid (Stripe)") },
17
+ { accessorKey: "currentPeriodStart", header: t("history.started", "Started"), cell: ({ row }) => date(row.original.currentPeriodStart) },
18
+ { accessorKey: "currentPeriodEnd", header: t("history.periodEnd", "Period end"), cell: ({ row }) => date(row.original.currentPeriodEnd) },
19
+ { accessorKey: "endedAt", header: t("history.ended", "Ended"), cell: ({ row }) => date(row.original.endedAt) }
20
+ ]);
21
+ async function changed() {
22
+ await refreshHistory();
23
+ emit("refresh");
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div class="flex flex-col gap-6">
29
+ <AdminSection :title="t('membership', 'Membership')">
30
+ <UsersAdminMembership
31
+ :user="user"
32
+ @refresh="changed"
33
+ />
34
+ </AdminSection>
35
+
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>
52
+ </div>
53
+ </template>
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ user: {
3
+ id: number;
4
+ name: string;
5
+ email: string;
6
+ };
7
+ };
8
+ declare const __VLS_export: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
9
+ refresh: () => any;
10
+ }, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
+ onRefresh?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,11 @@
1
+ type __VLS_Props = {
2
+ user: {
3
+ subscription?: {
4
+ planKey: string | null;
5
+ status: string;
6
+ } | null;
7
+ };
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>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,23 @@
1
+ <script setup>
2
+ defineOptions({ name: "UsersAdminCurrentSubscription" });
3
+ defineProps({
4
+ user: { type: Object, required: true }
5
+ });
6
+ const t = useModuleT("subscriptions.admin");
7
+ </script>
8
+
9
+ <template>
10
+ <AdminStatCard
11
+ :label="t('currentSubscription', 'Current subscription')"
12
+ :empty="t('none', 'No active membership.')"
13
+ >
14
+ <template v-if="user.subscription">
15
+ <UBadge
16
+ variant="subtle"
17
+ :color="['active', 'trialing'].includes(user.subscription.status) ? 'success' : 'neutral'"
18
+ :label="user.subscription.planKey ?? user.subscription.status"
19
+ />
20
+ <span class="text-muted">{{ user.subscription.status }}</span>
21
+ </template>
22
+ </AdminStatCard>
23
+ </template>
@@ -0,0 +1,11 @@
1
+ type __VLS_Props = {
2
+ user: {
3
+ subscription?: {
4
+ planKey: string | null;
5
+ status: string;
6
+ } | null;
7
+ };
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>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;