@frigear-nu/subscriptions 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +7 -3
- package/dist/runtime/app/components/SubscriptionPlans.vue +4 -4
- package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +2 -7
- package/dist/runtime/app/components/SubscriptionStatus.vue +2 -2
- package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +2 -7
- package/dist/runtime/app/composables/usePlans.d.ts +4 -11
- package/dist/runtime/app/composables/usePlans.js +7 -11
- package/dist/runtime/app/composables/useSubscription.d.ts +6 -24
- package/dist/runtime/app/composables/useSubscription.js +9 -15
- package/dist/runtime/app/middleware/subscription.js +2 -6
- package/dist/runtime/app/pages/membership.vue +16 -2
- package/dist/runtime/app/pages/success.vue +1 -1
- package/dist/runtime/app/tabs/Membership.vue +12 -2
- package/dist/runtime/app/widgets/MembershipSummary.vue +2 -2
- package/dist/runtime/i18n/lang/da.json +4 -1
- package/dist/runtime/i18n/lang/en.json +4 -1
- package/dist/runtime/server/bootstrap/my-subscriptions.d.ts +7 -0
- package/dist/runtime/server/bootstrap/my-subscriptions.js +4 -0
- package/dist/runtime/server/routes/subscriptions/me.get.js +2 -4
- package/package.json +14 -14
- package/dist/runtime/app/plugins/warm-plans.client.d.ts +0 -6
- package/dist/runtime/app/plugins/warm-plans.client.js +0 -12
package/README.md
CHANGED
|
@@ -119,8 +119,8 @@ Redirects to `redirectTo` (else the membership page) with `?redirect=<route>`; a
|
|
|
119
119
|
|
|
120
120
|
## Composables and components
|
|
121
121
|
|
|
122
|
-
- `usePlans()` - `{ plans, status, error, refresh }`.
|
|
123
|
-
- `useSubscription()` - `{ subscription, plan, status, isActive, cancelsAt, has(plan), pending, error, refresh, subscribe(planKeyOrPriceId, { successUrl?, cancelUrl? }), openPortal(returnUrl?) }`.
|
|
122
|
+
- `usePlans()` - `{ plans, status, loading, error, refresh }`.
|
|
123
|
+
- `useSubscription()` - `{ subscription, plan, status, loading, loadError, isActive, cancelsAt, has(plan), pending, error, refresh, subscribe(planKeyOrPriceId, { successUrl?, cancelUrl? }), openPortal(returnUrl?) }`.
|
|
124
124
|
- `<SubscriptionPlans>` - props `locale`, `currency`, `interval`, `plans`, `confirmChange`; slots `loading`,
|
|
125
125
|
`empty`, `header`, `badge`, `price`, `features`, `action`, `confirm`, `confirm-actions`.
|
|
126
126
|
- `<SubscriptionStatus>` - props `locale`; slots `header`, `loading`, `empty`, `plan`, `badge`, `dates`, `actions`.
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addServerImportsDir, addImports, addComponentsDir,
|
|
2
|
-
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, moduleApiUrl, inlineModuleRuntime, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addServerImportsDir, addImports, addComponentsDir, addRouteMiddleware } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, moduleApiUrl, registerBootstrapProvider, inlineModuleRuntime, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
|
|
3
3
|
import { addAuthScope } from '@frigear-nu/auth/utils';
|
|
4
4
|
import { extendUsersAdmin } from '@frigear-nu/users/utils';
|
|
5
5
|
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
@@ -97,6 +97,11 @@ const module$1 = defineNuxtModule({
|
|
|
97
97
|
method: "get",
|
|
98
98
|
handler: resolver.resolve("./runtime/server/routes/subscriptions/me.get")
|
|
99
99
|
});
|
|
100
|
+
registerBootstrapProvider(nuxt, {
|
|
101
|
+
scope: "account",
|
|
102
|
+
key: "subscriptions:me",
|
|
103
|
+
handler: resolver.resolve("./runtime/server/bootstrap/my-subscriptions")
|
|
104
|
+
});
|
|
100
105
|
addServerHandler({
|
|
101
106
|
route: moduleApiUrl("subscriptions", "checkout"),
|
|
102
107
|
method: "post",
|
|
@@ -139,7 +144,6 @@ const module$1 = defineNuxtModule({
|
|
|
139
144
|
{ name: "useSubscription", from: resolver.resolve("./runtime/app/composables/useSubscription") }
|
|
140
145
|
]);
|
|
141
146
|
addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
|
|
142
|
-
addPlugin({ src: resolver.resolve("./runtime/app/plugins/warm-plans.client"), mode: "client" });
|
|
143
147
|
addRouteMiddleware({
|
|
144
148
|
name: "subscription",
|
|
145
149
|
path: resolver.resolve("./runtime/app/middleware/subscription")
|
|
@@ -17,8 +17,8 @@ const emit = defineEmits(["select"]);
|
|
|
17
17
|
const t = useModuleT("subscriptions.plans");
|
|
18
18
|
const tp = useModuleT("subscriptions.catalog");
|
|
19
19
|
const ti = useModuleT("subscriptions.interval");
|
|
20
|
-
const { plans: allPlans,
|
|
21
|
-
const { plan: currentPlan, isActive, cancelsAt, pending, subscribe, error, status: subscriptionStatus } = useSubscription();
|
|
20
|
+
const { plans: allPlans, loading: plansLoading } = usePlans();
|
|
21
|
+
const { plan: currentPlan, isActive, cancelsAt, pending, subscribe, error, status: subscriptionStatus, loading: subscriptionLoading } = useSubscription();
|
|
22
22
|
const { loggedIn } = useUserSession();
|
|
23
23
|
const format = useFormat();
|
|
24
24
|
const route = useRoute();
|
|
@@ -196,7 +196,7 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
196
196
|
block
|
|
197
197
|
size="lg"
|
|
198
198
|
class="uppercase"
|
|
199
|
-
:loading="pending"
|
|
199
|
+
:loading="pending || loggedIn && subscriptionLoading"
|
|
200
200
|
:disabled="stateOf(plan) === 'current' || !priceFor(plan)"
|
|
201
201
|
:variant="stateOf(plan) === 'current' ? 'subtle' : 'solid'"
|
|
202
202
|
:label="loggedIn ? stateLabel(stateOf(plan)) : t('subscribe', 'Subscribe')"
|
|
@@ -208,7 +208,7 @@ watch([visible, subscriptionStatus, loggedIn], () => {
|
|
|
208
208
|
</DefineCard>
|
|
209
209
|
|
|
210
210
|
<slot
|
|
211
|
-
v-if="
|
|
211
|
+
v-if="plansLoading && !visible.length"
|
|
212
212
|
name="loading"
|
|
213
213
|
>
|
|
214
214
|
<p class="text-center text-sm text-muted">
|
|
@@ -13,13 +13,8 @@ declare var __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
|
13
13
|
cancelsAt: any;
|
|
14
14
|
dateText: any;
|
|
15
15
|
}, __VLS_25: {}, __VLS_33: {
|
|
16
|
-
manage: () =>
|
|
17
|
-
resubscribe: () =>
|
|
18
|
-
url: string;
|
|
19
|
-
} | {
|
|
20
|
-
updated: true;
|
|
21
|
-
kind: "upgrade" | "downgrade" | "resume";
|
|
22
|
-
}) | undefined>;
|
|
16
|
+
manage: () => any;
|
|
17
|
+
resubscribe: () => any;
|
|
23
18
|
cancelling: boolean;
|
|
24
19
|
pending: any;
|
|
25
20
|
};
|
|
@@ -10,7 +10,7 @@ const t = useModuleT("subscriptions.status");
|
|
|
10
10
|
const tp = useModuleT("subscriptions.catalog");
|
|
11
11
|
const format = useFormat();
|
|
12
12
|
const { plans } = usePlans();
|
|
13
|
-
const { subscription, plan,
|
|
13
|
+
const { subscription, plan, isActive, cancelsAt, loading, pending, error, subscribe, openPortal } = useSubscription();
|
|
14
14
|
const planLabel = computed(() => {
|
|
15
15
|
const found = plans.value.find((p) => p.key === plan.value);
|
|
16
16
|
return plan.value ? tp(`${plan.value}.label`, found?.label ?? plan.value) : plan.value;
|
|
@@ -49,7 +49,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
49
49
|
</template>
|
|
50
50
|
|
|
51
51
|
<slot
|
|
52
|
-
v-if="
|
|
52
|
+
v-if="loading"
|
|
53
53
|
name="loading"
|
|
54
54
|
>
|
|
55
55
|
<p class="text-muted text-sm">
|
|
@@ -13,13 +13,8 @@ declare var __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
|
13
13
|
cancelsAt: any;
|
|
14
14
|
dateText: any;
|
|
15
15
|
}, __VLS_25: {}, __VLS_33: {
|
|
16
|
-
manage: () =>
|
|
17
|
-
resubscribe: () =>
|
|
18
|
-
url: string;
|
|
19
|
-
} | {
|
|
20
|
-
updated: true;
|
|
21
|
-
kind: "upgrade" | "downgrade" | "resume";
|
|
22
|
-
}) | undefined>;
|
|
16
|
+
manage: () => any;
|
|
17
|
+
resubscribe: () => any;
|
|
23
18
|
cancelling: boolean;
|
|
24
19
|
pending: any;
|
|
25
20
|
};
|
|
@@ -21,14 +21,7 @@ export type PlansResponse = {
|
|
|
21
21
|
};
|
|
22
22
|
/** Matches the server-side KV cache (`CACHE_TTL_SECONDS`), so a client-side hit is never older than the server's own. */
|
|
23
23
|
export declare const PLANS_CLIENT_CACHE_MS: number;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export declare const usePlansCache: () => any;
|
|
29
|
-
export declare const usePlans: () => {
|
|
30
|
-
plans: any;
|
|
31
|
-
status: any;
|
|
32
|
-
error: any;
|
|
33
|
-
refresh: any;
|
|
34
|
-
};
|
|
24
|
+
/** The `useCachedFetch` key of the plans. */
|
|
25
|
+
export declare const PLANS_KEY = "subscriptions:plans";
|
|
26
|
+
/** The plans, sorted by rank. Awaitable, like `useSubscription`. */
|
|
27
|
+
export declare const usePlans: () => any;
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
export const PLANS_CLIENT_CACHE_MS = 5 * 60 * 1e3;
|
|
2
|
-
export const
|
|
2
|
+
export const PLANS_KEY = "subscriptions:plans";
|
|
3
3
|
export const usePlans = () => {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
}
|
|
4
|
+
const fetched = useCachedFetch("/api/subscriptions/plans", {
|
|
5
|
+
key: PLANS_KEY,
|
|
6
|
+
ttl: PLANS_CLIENT_CACHE_MS
|
|
13
7
|
});
|
|
8
|
+
const { data, status, error, loading, refresh } = fetched;
|
|
14
9
|
const plans = computed(
|
|
15
10
|
() => [...data.value?.plans ?? []].sort((a, b) => a.rank - b.rank)
|
|
16
11
|
);
|
|
17
|
-
|
|
12
|
+
const api = { plans, status, error, loading, refresh };
|
|
13
|
+
return Object.assign(fetched.then(() => api), api);
|
|
18
14
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { MaybeRefOrGetter } from 'vue';
|
|
2
1
|
export interface UserSubscription {
|
|
3
2
|
id: string;
|
|
4
3
|
planKey: string | null;
|
|
@@ -10,26 +9,9 @@ export interface UserSubscription {
|
|
|
10
9
|
endedAt: string | null;
|
|
11
10
|
source?: 'stripe' | 'manual';
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export declare const useSubscription: () => {
|
|
20
|
-
subscription: any;
|
|
21
|
-
plan: any;
|
|
22
|
-
status: any;
|
|
23
|
-
isActive: any;
|
|
24
|
-
cancelsAt: any;
|
|
25
|
-
has: (planKey: MaybeRefOrGetter<string>) => any;
|
|
26
|
-
pending: any;
|
|
27
|
-
error: any;
|
|
28
|
-
refresh: any;
|
|
29
|
-
subscribe: (planKeyOrPriceId: string, options?: {
|
|
30
|
-
successUrl?: string;
|
|
31
|
-
cancelUrl?: string;
|
|
32
|
-
}) => Promise<CheckoutResponse>;
|
|
33
|
-
openPortal: (returnUrl?: string) => Promise<void>;
|
|
34
|
-
};
|
|
35
|
-
export {};
|
|
12
|
+
/**
|
|
13
|
+
* The signed-in user's subscription and the checkout / billing-portal actions. Awaitable: `await useSubscription()` in
|
|
14
|
+
* a page renders the subscription on the server; components and widgets can use it without `await` and show a
|
|
15
|
+
* placeholder while `loading`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const useSubscription: () => any;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const ACTIVE_STATUSES = ["active", "trialing"];
|
|
2
2
|
const CLIENT_CACHE_MS = 60 * 1e3;
|
|
3
|
+
const ME_KEY = "subscriptions:me";
|
|
3
4
|
function isEntitled(item) {
|
|
4
5
|
if (!ACTIVE_STATUSES.includes(item.status) || item.endedAt) return false;
|
|
5
6
|
return !(item.source === "manual" && item.currentPeriodEnd && new Date(item.currentPeriodEnd).getTime() <= Date.now());
|
|
@@ -9,24 +10,16 @@ export const useSubscription = () => {
|
|
|
9
10
|
const t = useModuleT("subscriptions");
|
|
10
11
|
const pending = ref(false);
|
|
11
12
|
const error = ref(null);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
default: () => ({ subscriptions: [], activePlan: null }),
|
|
13
|
+
const fetched = useCachedFetch("/api/subscriptions/me", {
|
|
14
|
+
key: ME_KEY,
|
|
15
|
+
ttl: CLIENT_CACHE_MS,
|
|
16
16
|
immediate: loggedIn.value,
|
|
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
|
-
}
|
|
17
|
+
watch: false
|
|
23
18
|
});
|
|
19
|
+
const { data, status, error: loadError, loading, refresh, clear } = fetched;
|
|
24
20
|
watch(loggedIn, (value) => {
|
|
25
21
|
if (value) refresh();
|
|
26
|
-
else
|
|
27
|
-
cache.value = null;
|
|
28
|
-
data.value = { subscriptions: [], activePlan: null };
|
|
29
|
-
}
|
|
22
|
+
else clear();
|
|
30
23
|
});
|
|
31
24
|
const subscription = computed(() => {
|
|
32
25
|
const list = data.value?.subscriptions ?? [];
|
|
@@ -83,5 +76,6 @@ export const useSubscription = () => {
|
|
|
83
76
|
await navigateTo(url, { external: true });
|
|
84
77
|
}, t("errors.portal", "Failed to open the billing portal."));
|
|
85
78
|
}
|
|
86
|
-
|
|
79
|
+
const api = { subscription, plan, status, loading, loadError, isActive, cancelsAt, has, pending, error, refresh, subscribe, openPortal };
|
|
80
|
+
return Object.assign(fetched.then(() => api), api);
|
|
87
81
|
};
|
|
@@ -3,12 +3,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
3
3
|
const config = useRuntimeConfig().public.subscriptions;
|
|
4
4
|
const target = meta?.redirectTo ?? config?.pages?.membership;
|
|
5
5
|
if (!target || to.path === target) return;
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
if (loggedIn.value) {
|
|
9
|
-
const me = await useRequestFetch()("/api/subscriptions/me").catch(() => null);
|
|
10
|
-
activePlan = me?.activePlan ?? null;
|
|
11
|
-
}
|
|
6
|
+
const { plan } = await useSubscription();
|
|
7
|
+
const activePlan = plan.value;
|
|
12
8
|
const allowed = activePlan !== null && (!meta?.plans?.length || meta.plans.includes(activePlan));
|
|
13
9
|
if (!allowed) return navigateTo({ path: target, query: { redirect: to.fullPath } });
|
|
14
10
|
});
|
|
@@ -4,13 +4,27 @@ import SubscriptionStatus from "../components/SubscriptionStatus.vue";
|
|
|
4
4
|
import { useSubscription } from "../composables/useSubscription";
|
|
5
5
|
defineOptions({ name: "SubscriptionMembershipPage" });
|
|
6
6
|
const t = useModuleT("subscriptions");
|
|
7
|
-
const { isActive, cancelsAt } = useSubscription();
|
|
7
|
+
const { isActive, cancelsAt, loading, loadError, refresh } = await useSubscription();
|
|
8
8
|
const { loggedIn } = useUserSession();
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
12
|
<div class="flex flex-col gap-6">
|
|
13
|
-
|
|
13
|
+
<!-- Until the subscription is known, a placeholder the size of the status card: never the "join" hero. -->
|
|
14
|
+
<SectionCard
|
|
15
|
+
v-if="loading"
|
|
16
|
+
:title="t('status.title', 'Your subscription')"
|
|
17
|
+
icon="i-lucide-credit-card"
|
|
18
|
+
loading
|
|
19
|
+
/>
|
|
20
|
+
<SubscriptionStatus v-else-if="isActive" />
|
|
21
|
+
<UEmpty
|
|
22
|
+
v-else-if="loadError"
|
|
23
|
+
icon="i-lucide-circle-alert"
|
|
24
|
+
:title="t('status.loadFailed', 'Your membership could not be loaded')"
|
|
25
|
+
:description="errorMessage(loadError, '')"
|
|
26
|
+
:actions="[{ label: t('status.tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
27
|
+
/>
|
|
14
28
|
<div
|
|
15
29
|
v-else-if="loggedIn"
|
|
16
30
|
class="flex flex-col gap-1"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useSubscription } from "../composables/useSubscription";
|
|
3
3
|
defineOptions({ name: "SubscriptionSuccessPage" });
|
|
4
4
|
const t = useModuleT("subscriptions.success");
|
|
5
|
-
const { refresh, isActive } = useSubscription();
|
|
5
|
+
const { refresh, isActive } = await useSubscription();
|
|
6
6
|
const { localize } = useLocaleLink();
|
|
7
7
|
const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
|
|
8
8
|
const MAX_ATTEMPTS = 40;
|
|
@@ -7,10 +7,11 @@ const emit = defineEmits(["refresh"]);
|
|
|
7
7
|
const t = useModuleT("subscriptions.admin");
|
|
8
8
|
const format = useFormat();
|
|
9
9
|
const UBadge = resolveComponent("UBadge");
|
|
10
|
-
const { data: history, refresh: refreshHistory, status } = useLazyFetch(
|
|
10
|
+
const { data: history, refresh: refreshHistory, status, error } = useLazyFetch(
|
|
11
11
|
() => `/api/admin/users/${props.user.id}/subscriptions`,
|
|
12
12
|
{ key: () => `admin-subscriptions-${props.user.id}` }
|
|
13
13
|
);
|
|
14
|
+
const loading = computed(() => status.value === "pending" && !history.value);
|
|
14
15
|
const date = (value) => format.date(value) || "\u2013";
|
|
15
16
|
const columns = computed(() => [
|
|
16
17
|
{ accessorKey: "planKey", header: t("history.plan", "Plan"), cell: ({ row }) => row.original.planKey ?? row.original.priceId },
|
|
@@ -38,15 +39,24 @@ async function changed() {
|
|
|
38
39
|
<SectionCard
|
|
39
40
|
:title="t('history.title', 'Subscription history')"
|
|
40
41
|
flush
|
|
42
|
+
:loading="loading"
|
|
41
43
|
>
|
|
42
44
|
<UTable
|
|
43
45
|
:data="history?.subscriptions ?? []"
|
|
44
46
|
:columns="columns"
|
|
45
|
-
:loading="status === 'pending'"
|
|
47
|
+
:loading="status === 'pending' && !loading"
|
|
46
48
|
:ui="tableUi({ tr: '' })"
|
|
47
49
|
>
|
|
48
50
|
<template #empty>
|
|
49
51
|
<UEmpty
|
|
52
|
+
v-if="error"
|
|
53
|
+
variant="naked"
|
|
54
|
+
icon="i-lucide-circle-alert"
|
|
55
|
+
:title="t('history.loadFailed', 'The subscription history could not be loaded')"
|
|
56
|
+
:description="errorMessage(error, '')"
|
|
57
|
+
/>
|
|
58
|
+
<UEmpty
|
|
59
|
+
v-else
|
|
50
60
|
variant="naked"
|
|
51
61
|
icon="i-lucide-badge-check"
|
|
52
62
|
:title="t('history.empty', 'No subscriptions yet.')"
|
|
@@ -8,7 +8,7 @@ const tp = useModuleT("subscriptions.catalog");
|
|
|
8
8
|
const format = useFormat();
|
|
9
9
|
const { localize } = useLocaleLink();
|
|
10
10
|
const { plans } = usePlans();
|
|
11
|
-
const { subscription, plan,
|
|
11
|
+
const { subscription, plan, isActive, cancelsAt, loading } = useSubscription();
|
|
12
12
|
const membership = useRuntimeConfig().public.subscriptions?.pages?.membership;
|
|
13
13
|
const planLabel = computed(() => {
|
|
14
14
|
if (!plan.value) return "";
|
|
@@ -28,7 +28,7 @@ const hint = computed(() => {
|
|
|
28
28
|
:label="t('label', 'Membership')"
|
|
29
29
|
icon="i-lucide-credit-card"
|
|
30
30
|
:to="membership ? localize(membership) : void 0"
|
|
31
|
-
:loading="
|
|
31
|
+
:loading="loading"
|
|
32
32
|
:value="isActive ? planLabel : void 0"
|
|
33
33
|
:hint="isActive ? hint : void 0"
|
|
34
34
|
:empty="t('none', 'No active membership')"
|
|
@@ -56,7 +56,9 @@
|
|
|
56
56
|
"free": "Gratis medlemskab",
|
|
57
57
|
"freeUntil": "Gratis medlemskab til {date}",
|
|
58
58
|
"heroTitle": "Bliv en del af holdet i dag",
|
|
59
|
-
"heroDescription": "Opret dit medlemskab for at komme i gang!"
|
|
59
|
+
"heroDescription": "Opret dit medlemskab for at komme i gang!",
|
|
60
|
+
"loadFailed": "Dit medlemskab kunne ikke indlæses",
|
|
61
|
+
"tryAgain": "Prøv igen"
|
|
60
62
|
},
|
|
61
63
|
"success": {
|
|
62
64
|
"title": "Tak!",
|
|
@@ -107,6 +109,7 @@
|
|
|
107
109
|
"currentSubscription": "Nuværende abonnement",
|
|
108
110
|
"history": {
|
|
109
111
|
"title": "Abonnementshistorik",
|
|
112
|
+
"loadFailed": "Abonnementshistorikken kunne ikke hentes",
|
|
110
113
|
"empty": "Ingen abonnementer endnu.",
|
|
111
114
|
"plan": "Plan",
|
|
112
115
|
"status": "Status",
|
|
@@ -56,7 +56,9 @@
|
|
|
56
56
|
"free": "Free membership",
|
|
57
57
|
"freeUntil": "Free membership until {date}",
|
|
58
58
|
"heroTitle": "Join the team today",
|
|
59
|
-
"heroDescription": "Set up your membership to get started!"
|
|
59
|
+
"heroDescription": "Set up your membership to get started!",
|
|
60
|
+
"loadFailed": "Your membership could not be loaded",
|
|
61
|
+
"tryAgain": "Try again"
|
|
60
62
|
},
|
|
61
63
|
"success": {
|
|
62
64
|
"title": "Thank you!",
|
|
@@ -107,6 +109,7 @@
|
|
|
107
109
|
"currentSubscription": "Current subscription",
|
|
108
110
|
"history": {
|
|
109
111
|
"title": "Subscription history",
|
|
112
|
+
"loadFailed": "The subscription history could not be loaded",
|
|
110
113
|
"empty": "No subscriptions yet.",
|
|
111
114
|
"plan": "Plan",
|
|
112
115
|
"status": "Status",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { MeResponse } from '../../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The caller's subscriptions: the body of `GET /api/subscriptions/me` and the account bootstrap's `subscriptions:me`
|
|
5
|
+
* (the key `useSubscription()` reads), so both always return the same thing.
|
|
6
|
+
*/
|
|
7
|
+
export default function mySubscriptions(event: H3Event): Promise<MeResponse>;
|
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return listUserSubscriptions(user.id);
|
|
4
|
-
});
|
|
1
|
+
import mySubscriptions from "../../bootstrap/my-subscriptions.js";
|
|
2
|
+
export default defineEventHandler(mySubscriptions);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/subscriptions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,13 +30,13 @@
|
|
|
30
30
|
"playground"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@frigear-nu/auth": "0.1.
|
|
34
|
-
"@frigear-nu/mcp": "0.1.
|
|
35
|
-
"@frigear-nu/module-kit": "1.1.
|
|
36
|
-
"@frigear-nu/notify": "0.1.
|
|
37
|
-
"@frigear-nu/stripe": "0.1.
|
|
38
|
-
"@frigear-nu/ui": "0.1.
|
|
39
|
-
"@frigear-nu/users": "1.1.
|
|
33
|
+
"@frigear-nu/auth": "0.1.3",
|
|
34
|
+
"@frigear-nu/mcp": "0.1.2",
|
|
35
|
+
"@frigear-nu/module-kit": "1.1.2",
|
|
36
|
+
"@frigear-nu/notify": "0.1.2",
|
|
37
|
+
"@frigear-nu/stripe": "0.1.2",
|
|
38
|
+
"@frigear-nu/ui": "0.1.2",
|
|
39
|
+
"@frigear-nu/users": "1.1.3",
|
|
40
40
|
"@libsql/client": "^0.18.0",
|
|
41
41
|
"@nuxt/kit": "^4.6.1",
|
|
42
42
|
"@vueuse/core": "^15.0.0",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@frigear-nu/email-templates": "0.1.0",
|
|
52
|
-
"@frigear-nu/menu": "0.1.
|
|
53
|
-
"@frigear-nu/widget": "0.1.
|
|
54
|
-
"@nuxt/ui": "^4.
|
|
52
|
+
"@frigear-nu/menu": "0.1.2",
|
|
53
|
+
"@frigear-nu/widget": "0.1.2",
|
|
54
|
+
"@nuxt/ui": "^4.12.0",
|
|
55
55
|
"@nuxthub/core": "^0.10.8"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@frigear-nu/email-templates": "0.1.0",
|
|
73
|
-
"@frigear-nu/menu": "0.1.
|
|
74
|
-
"@frigear-nu/widget": "0.1.
|
|
73
|
+
"@frigear-nu/menu": "0.1.2",
|
|
74
|
+
"@frigear-nu/widget": "0.1.2",
|
|
75
75
|
"@nuxt/devtools": "^3.4.2",
|
|
76
76
|
"@nuxt/eslint-config": "^1.17.0",
|
|
77
77
|
"@nuxt/module-builder": "^1.0.3",
|
|
78
78
|
"@nuxt/schema": "^4.6.1",
|
|
79
79
|
"@nuxt/test-utils": "^4.3.2",
|
|
80
|
-
"@nuxt/ui": "^4.
|
|
80
|
+
"@nuxt/ui": "^4.12.0",
|
|
81
81
|
"@nuxthub/core": "^0.10.8",
|
|
82
82
|
"@types/node": "^26.6.5",
|
|
83
83
|
"changelogen": "^0.6.2",
|
|
@@ -1,6 +0,0 @@
|
|
|
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;
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
});
|