@frigear-nu/subscriptions 0.0.7 → 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.
- package/dist/module.d.mts +65 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +60 -29
- package/dist/runtime/app/components/SubscriptionPlans.d.vue.ts +31 -27
- package/dist/runtime/app/components/SubscriptionPlans.vue +203 -105
- package/dist/runtime/app/components/SubscriptionPlans.vue.d.ts +31 -27
- package/dist/runtime/app/components/SubscriptionStatus.d.vue.ts +1 -1
- package/dist/runtime/app/components/SubscriptionStatus.vue +45 -24
- package/dist/runtime/app/components/SubscriptionStatus.vue.d.ts +1 -1
- package/dist/runtime/app/components/Users/Admin/Membership.d.vue.ts +14 -0
- package/dist/runtime/app/components/Users/Admin/Membership.vue +192 -0
- package/dist/runtime/app/components/Users/Admin/Membership.vue.d.ts +14 -0
- package/dist/runtime/app/composables/usePlans.d.ts +11 -0
- package/dist/runtime/app/composables/usePlans.js +9 -1
- package/dist/runtime/app/composables/useSubscription.d.ts +1 -0
- package/dist/runtime/app/composables/useSubscription.js +17 -3
- package/dist/runtime/app/pages/cancel.d.vue.ts +1 -1
- package/dist/runtime/app/pages/cancel.vue.d.ts +1 -1
- package/dist/runtime/app/pages/membership.d.vue.ts +1 -1
- package/dist/runtime/app/pages/membership.vue +18 -1
- package/dist/runtime/app/pages/membership.vue.d.ts +1 -1
- package/dist/runtime/app/pages/success.d.vue.ts +1 -1
- package/dist/runtime/app/pages/success.vue +9 -3
- package/dist/runtime/app/pages/success.vue.d.ts +1 -1
- package/dist/runtime/app/plugins/warm-plans.client.d.ts +6 -0
- package/dist/runtime/app/plugins/warm-plans.client.js +12 -0
- package/dist/runtime/app/tabs/Membership.d.vue.ts +14 -0
- package/dist/runtime/app/tabs/Membership.vue +53 -0
- package/dist/runtime/app/tabs/Membership.vue.d.ts +14 -0
- package/dist/runtime/app/widgets/AdminCurrentSubscription.d.vue.ts +11 -0
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue +23 -0
- package/dist/runtime/app/widgets/AdminCurrentSubscription.vue.d.ts +11 -0
- package/dist/runtime/i18n/lang/da.json +61 -3
- package/dist/runtime/i18n/lang/en.json +61 -3
- package/dist/runtime/server/db/schema.sqlite.d.ts +19 -0
- package/dist/runtime/server/db/schema.sqlite.js +2 -0
- package/dist/runtime/server/mcp/tools/my-subscriptions.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/my-subscriptions.js +9 -0
- package/dist/runtime/server/routes/admin/payment-link.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/payment-link.post.js +46 -0
- package/dist/runtime/server/routes/admin/subscription.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscription.delete.js +14 -0
- package/dist/runtime/server/routes/admin/subscription.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscription.get.js +17 -0
- package/dist/runtime/server/routes/admin/subscription.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscription.post.js +44 -0
- package/dist/runtime/server/routes/admin/subscriptions.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/subscriptions.get.js +4 -0
- package/dist/runtime/server/routes/subscriptions/checkout.post.js +16 -30
- package/dist/runtime/server/routes/subscriptions/me.get.js +2 -20
- package/dist/runtime/server/routes/subscriptions/portal.post.js +2 -2
- package/dist/runtime/server/stripe/invoice-events.d.ts +2 -0
- package/dist/runtime/server/stripe/invoice-events.js +42 -0
- package/dist/runtime/server/utils/admin-membership.d.ts +5 -0
- package/dist/runtime/server/utils/admin-membership.js +7 -0
- package/dist/runtime/server/utils/checkout.d.ts +15 -0
- package/dist/runtime/server/utils/checkout.js +22 -0
- package/dist/runtime/server/utils/plans.js +20 -1
- package/dist/runtime/server/utils/subscriptions.d.ts +3 -0
- package/dist/runtime/server/utils/subscriptions.js +22 -3
- package/dist/runtime/utils/index.d.ts +1 -1
- package/dist/runtime/utils/index.js +1 -1
- package/dist/runtime/utils/plans.d.ts +15 -6
- package/dist/runtime/utils/plans.js +16 -11
- package/dist/types.d.mts +1 -1
- package/package.json +17 -5
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { createReusableTemplate } from "@vueuse/core";
|
|
3
|
+
import { formatMoney } from "@frigear-nu/ui/money";
|
|
2
4
|
import { usePlans } from "../composables/usePlans";
|
|
3
5
|
import { useSubscription } from "../composables/useSubscription";
|
|
4
6
|
defineOptions({ name: "SubscriptionPlans" });
|
|
@@ -7,14 +9,19 @@ const props = defineProps({
|
|
|
7
9
|
currency: { type: String, required: false },
|
|
8
10
|
interval: { type: String, required: false },
|
|
9
11
|
plans: { type: Array, required: false },
|
|
10
|
-
confirmChange: { type: Boolean, required: false, default: false }
|
|
12
|
+
confirmChange: { type: Boolean, required: false, default: false },
|
|
13
|
+
mode: { type: String, required: false, default: "auto" },
|
|
14
|
+
highlight: { type: String, required: false }
|
|
11
15
|
});
|
|
12
16
|
const emit = defineEmits(["select"]);
|
|
13
17
|
const t = useModuleT("subscriptions.plans");
|
|
14
18
|
const tp = useModuleT("subscriptions.catalog");
|
|
15
19
|
const ti = useModuleT("subscriptions.interval");
|
|
16
20
|
const { plans: allPlans, status } = usePlans();
|
|
17
|
-
const { plan: currentPlan, isActive, pending, subscribe, error } = useSubscription();
|
|
21
|
+
const { plan: currentPlan, isActive, cancelsAt, pending, subscribe, error, status: subscriptionStatus } = useSubscription();
|
|
22
|
+
const { loggedIn } = useUserSession();
|
|
23
|
+
const route = useRoute();
|
|
24
|
+
const [DefineCard, ReuseCard] = createReusableTemplate();
|
|
18
25
|
const visible = computed(() => allPlans.value.filter((p) => !props.plans || props.plans.includes(p.key)));
|
|
19
26
|
const currentRank = computed(() => allPlans.value.find((p) => p.key === currentPlan.value)?.rank ?? null);
|
|
20
27
|
function priceFor(plan) {
|
|
@@ -22,14 +29,12 @@ function priceFor(plan) {
|
|
|
22
29
|
}
|
|
23
30
|
function formatPrice(price) {
|
|
24
31
|
if (price.amount === null) return "";
|
|
25
|
-
return
|
|
26
|
-
style: "currency",
|
|
27
|
-
currency: (props.currency ?? price.currency).toUpperCase()
|
|
28
|
-
}).format(price.amount / 100);
|
|
32
|
+
return formatMoney(price.amount, props.currency ?? price.currency, props.locale);
|
|
29
33
|
}
|
|
30
34
|
function intervalLabel(price) {
|
|
31
35
|
const singular = { day: "day", week: "week", month: "month", year: "year" };
|
|
32
36
|
const unit = singular[price.interval];
|
|
37
|
+
if (price.interval === "month" && price.intervalCount === 3) return ti("quarter", "quarter");
|
|
33
38
|
if (price.intervalCount <= 1) return ti(unit, unit);
|
|
34
39
|
return `${price.intervalCount} ${ti(`${unit}s`, `${unit}s`)}`;
|
|
35
40
|
}
|
|
@@ -37,11 +42,28 @@ const labelOf = (plan) => tp(`${plan.key}.label`, plan.label);
|
|
|
37
42
|
const descriptionOf = (plan) => plan.description ? tp(`${plan.key}.description`, plan.description) : "";
|
|
38
43
|
function stateOf(plan) {
|
|
39
44
|
if (!isActive.value) return "subscribe";
|
|
40
|
-
if (plan.key === currentPlan.value) return "current";
|
|
45
|
+
if (plan.key === currentPlan.value) return cancelsAt.value ? "resume" : "current";
|
|
41
46
|
if (currentRank.value === null) return "subscribe";
|
|
42
47
|
return plan.rank > currentRank.value ? "upgrade" : "downgrade";
|
|
43
48
|
}
|
|
44
|
-
const stateLabel = (state) => t(state, { current: "Current plan", subscribe: "Subscribe", upgrade: "Upgrade", downgrade: "Downgrade" }[state]);
|
|
49
|
+
const stateLabel = (state) => t(state, { current: "Current plan", resume: "Resubscribe", subscribe: "Subscribe", upgrade: "Upgrade", downgrade: "Downgrade" }[state]);
|
|
50
|
+
const showTabs = computed(() => props.mode !== "list");
|
|
51
|
+
const showGrid = computed(() => props.mode !== "tabs");
|
|
52
|
+
const activeTab = ref();
|
|
53
|
+
const tabItems = computed(() => visible.value.map((plan) => ({
|
|
54
|
+
value: plan.key,
|
|
55
|
+
label: priceFor(plan) ? `/ ${intervalLabel(priceFor(plan))}` : labelOf(plan),
|
|
56
|
+
plan
|
|
57
|
+
})));
|
|
58
|
+
watchEffect(() => {
|
|
59
|
+
if (!activeTab.value || !visible.value.some((p) => p.key === activeTab.value)) {
|
|
60
|
+
activeTab.value = (visible.value.find((p) => p.key === currentPlan.value) ?? visible.value[visible.value.length - 1])?.key;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const highlighted = (plan) => props.highlight ? props.highlight === plan.key : stateOf(plan) === "current";
|
|
64
|
+
function featuresOf(plan) {
|
|
65
|
+
return plan.features?.length ? plan.features : void 0;
|
|
66
|
+
}
|
|
45
67
|
const confirming = ref(null);
|
|
46
68
|
const confirmOpen = computed({
|
|
47
69
|
get: () => confirming.value !== null,
|
|
@@ -54,16 +76,135 @@ async function go(plan, price) {
|
|
|
54
76
|
confirming.value = null;
|
|
55
77
|
await subscribe(plan.key).catch(() => void 0);
|
|
56
78
|
}
|
|
79
|
+
function signUpTarget(plan) {
|
|
80
|
+
const authPages = useAuthPages();
|
|
81
|
+
const pages = useRuntimeConfig().public;
|
|
82
|
+
const entry = authPages.register ?? authPages.login;
|
|
83
|
+
const back = `${pages.subscriptions?.pages?.membership ?? route.path}?plan=${encodeURIComponent(plan.key)}`;
|
|
84
|
+
return entry ? `${entry}?redirect=${encodeURIComponent(back)}` : null;
|
|
85
|
+
}
|
|
57
86
|
function choose(plan) {
|
|
58
87
|
const price = priceFor(plan);
|
|
59
88
|
if (!price) return;
|
|
89
|
+
if (!loggedIn.value) {
|
|
90
|
+
const target = signUpTarget(plan);
|
|
91
|
+
if (target) navigateTo(target);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (stateOf(plan) === "current") return;
|
|
60
95
|
if (props.confirmChange && isActive.value) confirming.value = { plan, price };
|
|
61
96
|
else go(plan, price);
|
|
62
97
|
}
|
|
98
|
+
const confirmKind = computed(() => confirming.value ? stateOf(confirming.value.plan) : "subscribe");
|
|
99
|
+
const confirmText = computed(() => ({
|
|
100
|
+
upgrade: t("confirmUpgrade", "Upgrading moves you to the higher plan immediately and charges the difference."),
|
|
101
|
+
downgrade: t("confirmDowngrade", "Downgrading keeps your current plan until the next due date."),
|
|
102
|
+
resume: t("confirmResume", "Your membership will keep renewing as before."),
|
|
103
|
+
subscribe: t("confirmCheckout", "You will now be taken to Stripe to confirm."),
|
|
104
|
+
current: ""
|
|
105
|
+
})[confirmKind.value]);
|
|
106
|
+
const handledPlanQuery = ref(false);
|
|
107
|
+
watch([visible, subscriptionStatus, loggedIn], () => {
|
|
108
|
+
const key = route.query.plan;
|
|
109
|
+
if (handledPlanQuery.value || typeof key !== "string" || !loggedIn.value || subscriptionStatus.value === "pending") return;
|
|
110
|
+
const plan = visible.value.find((p) => p.key === key);
|
|
111
|
+
if (!plan) return;
|
|
112
|
+
handledPlanQuery.value = true;
|
|
113
|
+
if (stateOf(plan) !== "current") choose(plan);
|
|
114
|
+
navigateTo({ path: route.path, query: { ...route.query, plan: void 0 } }, { replace: true });
|
|
115
|
+
}, { immediate: true });
|
|
63
116
|
</script>
|
|
64
117
|
|
|
65
118
|
<template>
|
|
66
119
|
<div class="space-y-4">
|
|
120
|
+
<DefineCard v-slot="{ plan, preview }">
|
|
121
|
+
<UPricingPlan
|
|
122
|
+
:data-plan="plan.key"
|
|
123
|
+
:title="labelOf(plan)"
|
|
124
|
+
:description="descriptionOf(plan)"
|
|
125
|
+
:price="priceFor(plan) ? formatPrice(priceFor(plan)) : void 0"
|
|
126
|
+
:billing-cycle="priceFor(plan) ? `/ ${intervalLabel(priceFor(plan))}` : void 0"
|
|
127
|
+
:features="featuresOf(plan)"
|
|
128
|
+
:terms="t('feeIncluded', '*Payment fees are included in the amount')"
|
|
129
|
+
:highlight="!preview && highlighted(plan)"
|
|
130
|
+
:badge="!preview && stateOf(plan) === 'current' ? t('current', 'Current plan') : void 0"
|
|
131
|
+
:tagline="priceFor(plan)?.trialPeriodDays ? t('trial', '{days}-day free trial', { days: priceFor(plan).trialPeriodDays }) : void 0"
|
|
132
|
+
variant="subtle"
|
|
133
|
+
:ui="{
|
|
134
|
+
root: 'rounded-xl transition-shadow hover:shadow-lg',
|
|
135
|
+
title: 'text-lg font-semibold',
|
|
136
|
+
price: 'text-4xl font-extrabold text-primary',
|
|
137
|
+
billingCycle: 'uppercase ml-1 text-muted',
|
|
138
|
+
terms: 'text-xs text-muted'
|
|
139
|
+
}"
|
|
140
|
+
>
|
|
141
|
+
<template
|
|
142
|
+
v-if="$slots.header"
|
|
143
|
+
#header
|
|
144
|
+
>
|
|
145
|
+
<slot
|
|
146
|
+
name="header"
|
|
147
|
+
:plan="plan"
|
|
148
|
+
:state="stateOf(plan)"
|
|
149
|
+
/>
|
|
150
|
+
</template>
|
|
151
|
+
<template
|
|
152
|
+
v-if="$slots.badge"
|
|
153
|
+
#badge
|
|
154
|
+
>
|
|
155
|
+
<slot
|
|
156
|
+
name="badge"
|
|
157
|
+
:plan="plan"
|
|
158
|
+
:state="stateOf(plan)"
|
|
159
|
+
/>
|
|
160
|
+
</template>
|
|
161
|
+
<template
|
|
162
|
+
v-if="$slots.price"
|
|
163
|
+
#price
|
|
164
|
+
>
|
|
165
|
+
<slot
|
|
166
|
+
name="price"
|
|
167
|
+
:plan="plan"
|
|
168
|
+
:price="priceFor(plan)"
|
|
169
|
+
:format-price="formatPrice"
|
|
170
|
+
:interval-label="intervalLabel"
|
|
171
|
+
/>
|
|
172
|
+
</template>
|
|
173
|
+
<template
|
|
174
|
+
v-if="$slots.features"
|
|
175
|
+
#features
|
|
176
|
+
>
|
|
177
|
+
<slot
|
|
178
|
+
name="features"
|
|
179
|
+
:plan="plan"
|
|
180
|
+
/>
|
|
181
|
+
</template>
|
|
182
|
+
<template
|
|
183
|
+
v-if="!preview"
|
|
184
|
+
#button
|
|
185
|
+
>
|
|
186
|
+
<slot
|
|
187
|
+
name="action"
|
|
188
|
+
:plan="plan"
|
|
189
|
+
:state="stateOf(plan)"
|
|
190
|
+
:pending="pending"
|
|
191
|
+
:select="() => choose(plan)"
|
|
192
|
+
>
|
|
193
|
+
<UButton
|
|
194
|
+
block
|
|
195
|
+
size="lg"
|
|
196
|
+
class="uppercase"
|
|
197
|
+
:loading="pending"
|
|
198
|
+
:disabled="stateOf(plan) === 'current' || !priceFor(plan)"
|
|
199
|
+
:variant="stateOf(plan) === 'current' ? 'subtle' : 'solid'"
|
|
200
|
+
:label="loggedIn ? stateLabel(stateOf(plan)) : t('subscribe', 'Subscribe')"
|
|
201
|
+
@click="choose(plan)"
|
|
202
|
+
/>
|
|
203
|
+
</slot>
|
|
204
|
+
</template>
|
|
205
|
+
</UPricingPlan>
|
|
206
|
+
</DefineCard>
|
|
207
|
+
|
|
67
208
|
<slot
|
|
68
209
|
v-if="status === 'pending' && !visible.length"
|
|
69
210
|
name="loading"
|
|
@@ -89,98 +230,41 @@ function choose(plan) {
|
|
|
89
230
|
:description="error"
|
|
90
231
|
/>
|
|
91
232
|
|
|
92
|
-
<
|
|
93
|
-
v-if="
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
:plan="plan"
|
|
105
|
-
:state="stateOf(plan)"
|
|
106
|
-
>
|
|
107
|
-
<div class="flex items-center justify-between gap-2">
|
|
108
|
-
<h3 class="font-semibold">
|
|
109
|
-
{{ labelOf(plan) }}
|
|
110
|
-
</h3>
|
|
111
|
-
<slot
|
|
112
|
-
name="badge"
|
|
113
|
-
:plan="plan"
|
|
114
|
-
:state="stateOf(plan)"
|
|
115
|
-
>
|
|
116
|
-
<UBadge
|
|
117
|
-
v-if="stateOf(plan) === 'current'"
|
|
118
|
-
color="primary"
|
|
119
|
-
variant="subtle"
|
|
120
|
-
>
|
|
121
|
-
{{ t("current", "Current plan") }}
|
|
122
|
-
</UBadge>
|
|
123
|
-
</slot>
|
|
124
|
-
</div>
|
|
125
|
-
<p
|
|
126
|
-
v-if="descriptionOf(plan)"
|
|
127
|
-
class="mt-1 text-sm text-muted"
|
|
128
|
-
>
|
|
129
|
-
{{ descriptionOf(plan) }}
|
|
130
|
-
</p>
|
|
131
|
-
</slot>
|
|
132
|
-
</template>
|
|
133
|
-
|
|
134
|
-
<slot
|
|
135
|
-
name="price"
|
|
136
|
-
:plan="plan"
|
|
137
|
-
:price="priceFor(plan)"
|
|
138
|
-
:format-price="formatPrice"
|
|
139
|
-
:interval-label="intervalLabel"
|
|
233
|
+
<template v-if="visible.length">
|
|
234
|
+
<template v-if="showTabs">
|
|
235
|
+
<UTabs
|
|
236
|
+
v-model="activeTab"
|
|
237
|
+
:items="tabItems"
|
|
238
|
+
:content="false"
|
|
239
|
+
class="w-full"
|
|
240
|
+
:class="{ 'sm:hidden': showGrid }"
|
|
241
|
+
/>
|
|
242
|
+
<div
|
|
243
|
+
class="pt-2"
|
|
244
|
+
:class="{ 'sm:hidden': showGrid }"
|
|
140
245
|
>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<slot
|
|
156
|
-
name="features"
|
|
246
|
+
<ReuseCard
|
|
247
|
+
v-if="visible.find((p) => p.key === activeTab)"
|
|
248
|
+
:plan="visible.find((p) => p.key === activeTab)"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
</template>
|
|
252
|
+
<div
|
|
253
|
+
v-if="showGrid"
|
|
254
|
+
class="items-stretch gap-6 sm:grid-cols-2"
|
|
255
|
+
:class="[showTabs ? 'hidden sm:grid' : 'grid', visible.length >= 3 ? 'lg:grid-cols-3' : '']"
|
|
256
|
+
>
|
|
257
|
+
<ReuseCard
|
|
258
|
+
v-for="plan in visible"
|
|
259
|
+
:key="plan.key"
|
|
157
260
|
:plan="plan"
|
|
158
261
|
/>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<slot
|
|
162
|
-
name="action"
|
|
163
|
-
:plan="plan"
|
|
164
|
-
:state="stateOf(plan)"
|
|
165
|
-
:pending="pending"
|
|
166
|
-
:select="() => choose(plan)"
|
|
167
|
-
>
|
|
168
|
-
<UButton
|
|
169
|
-
block
|
|
170
|
-
:loading="pending"
|
|
171
|
-
:disabled="stateOf(plan) === 'current' || !priceFor(plan)"
|
|
172
|
-
:variant="stateOf(plan) === 'current' ? 'outline' : 'solid'"
|
|
173
|
-
:label="stateLabel(stateOf(plan))"
|
|
174
|
-
@click="choose(plan)"
|
|
175
|
-
/>
|
|
176
|
-
</slot>
|
|
177
|
-
</template>
|
|
178
|
-
</UCard>
|
|
179
|
-
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</template>
|
|
180
264
|
|
|
181
265
|
<UModal
|
|
182
266
|
v-model:open="confirmOpen"
|
|
183
|
-
:title="t('
|
|
267
|
+
:title="confirmKind === 'subscribe' ? t('checkoutTitle', 'Get a membership') : t('changeTitle', 'Update subscription')"
|
|
184
268
|
>
|
|
185
269
|
<template #body>
|
|
186
270
|
<slot
|
|
@@ -188,7 +272,19 @@ function choose(plan) {
|
|
|
188
272
|
:plan="confirming?.plan"
|
|
189
273
|
:price="confirming?.price"
|
|
190
274
|
>
|
|
191
|
-
<
|
|
275
|
+
<div class="space-y-4">
|
|
276
|
+
<p class="text-muted">
|
|
277
|
+
{{ t("subscribeTo", "You will now subscribe to:") }}
|
|
278
|
+
</p>
|
|
279
|
+
<ReuseCard
|
|
280
|
+
v-if="confirming"
|
|
281
|
+
:plan="confirming.plan"
|
|
282
|
+
preview
|
|
283
|
+
/>
|
|
284
|
+
<p class="text-sm text-muted">
|
|
285
|
+
{{ confirmText }}
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
192
288
|
</slot>
|
|
193
289
|
</template>
|
|
194
290
|
<template #footer>
|
|
@@ -198,16 +294,18 @@ function choose(plan) {
|
|
|
198
294
|
:price="confirming?.price"
|
|
199
295
|
:confirm="() => confirming && go(confirming.plan, confirming.price)"
|
|
200
296
|
>
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
297
|
+
<div class="flex w-full justify-end gap-2">
|
|
298
|
+
<UButton
|
|
299
|
+
variant="outline"
|
|
300
|
+
:label="t('cancel', 'Cancel')"
|
|
301
|
+
@click="confirming = null"
|
|
302
|
+
/>
|
|
303
|
+
<UButton
|
|
304
|
+
:label="t('confirm', 'Confirm')"
|
|
305
|
+
:loading="pending"
|
|
306
|
+
@click="confirming && go(confirming.plan, confirming.price)"
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
211
309
|
</slot>
|
|
212
310
|
</template>
|
|
213
311
|
</UModal>
|
|
@@ -8,59 +8,63 @@ type __VLS_Props = {
|
|
|
8
8
|
plans?: string[];
|
|
9
9
|
/** Ask for confirmation (modal) before changing an existing subscription. */
|
|
10
10
|
confirmChange?: boolean;
|
|
11
|
+
/** `list` shows all plans side by side, `tabs` one at a time. `auto` uses tabs on small screens. */
|
|
12
|
+
mode?: 'auto' | 'list' | 'tabs';
|
|
13
|
+
/** Plan key to emphasise (defaults to the current plan, when subscribed). */
|
|
14
|
+
highlight?: string;
|
|
11
15
|
};
|
|
12
16
|
declare function formatPrice(price: SubscriptionPlanPrice): string;
|
|
13
17
|
declare function intervalLabel(price: SubscriptionPlanPrice): any;
|
|
14
|
-
declare var
|
|
15
|
-
plan:
|
|
16
|
-
state: "upgrade" | "downgrade" | "subscribe" | "current";
|
|
17
|
-
},
|
|
18
|
-
plan:
|
|
19
|
-
state: "upgrade" | "downgrade" | "subscribe" | "current";
|
|
20
|
-
},
|
|
21
|
-
plan:
|
|
18
|
+
declare var __VLS_14: {
|
|
19
|
+
plan: SubscriptionPlan;
|
|
20
|
+
state: "upgrade" | "downgrade" | "resume" | "subscribe" | "current";
|
|
21
|
+
}, __VLS_17: {
|
|
22
|
+
plan: SubscriptionPlan;
|
|
23
|
+
state: "upgrade" | "downgrade" | "resume" | "subscribe" | "current";
|
|
24
|
+
}, __VLS_20: {
|
|
25
|
+
plan: SubscriptionPlan;
|
|
22
26
|
price: SubscriptionPlanPrice | undefined;
|
|
23
27
|
formatPrice: typeof formatPrice;
|
|
24
28
|
intervalLabel: typeof intervalLabel;
|
|
25
|
-
},
|
|
26
|
-
plan:
|
|
27
|
-
},
|
|
28
|
-
plan:
|
|
29
|
-
state: "upgrade" | "downgrade" | "subscribe" | "current";
|
|
29
|
+
}, __VLS_23: {
|
|
30
|
+
plan: SubscriptionPlan;
|
|
31
|
+
}, __VLS_26: {
|
|
32
|
+
plan: SubscriptionPlan;
|
|
33
|
+
state: "upgrade" | "downgrade" | "resume" | "subscribe" | "current";
|
|
30
34
|
pending: any;
|
|
31
35
|
select: () => void;
|
|
32
|
-
},
|
|
36
|
+
}, __VLS_35: {}, __VLS_37: {}, __VLS_66: {
|
|
33
37
|
plan: any;
|
|
34
38
|
price: any;
|
|
35
|
-
},
|
|
39
|
+
}, __VLS_74: {
|
|
36
40
|
plan: any;
|
|
37
41
|
price: any;
|
|
38
42
|
confirm: () => any;
|
|
39
43
|
};
|
|
40
44
|
type __VLS_Slots = {} & {
|
|
41
|
-
|
|
45
|
+
header?: (props: typeof __VLS_14) => any;
|
|
42
46
|
} & {
|
|
43
|
-
|
|
47
|
+
badge?: (props: typeof __VLS_17) => any;
|
|
44
48
|
} & {
|
|
45
|
-
|
|
49
|
+
price?: (props: typeof __VLS_20) => any;
|
|
46
50
|
} & {
|
|
47
|
-
|
|
51
|
+
features?: (props: typeof __VLS_23) => any;
|
|
48
52
|
} & {
|
|
49
|
-
|
|
53
|
+
action?: (props: typeof __VLS_26) => any;
|
|
50
54
|
} & {
|
|
51
|
-
|
|
55
|
+
loading?: (props: typeof __VLS_35) => any;
|
|
52
56
|
} & {
|
|
53
|
-
|
|
57
|
+
empty?: (props: typeof __VLS_37) => any;
|
|
54
58
|
} & {
|
|
55
|
-
confirm?: (props: typeof
|
|
59
|
+
confirm?: (props: typeof __VLS_66) => any;
|
|
56
60
|
} & {
|
|
57
|
-
'confirm-actions'?: (props: typeof
|
|
61
|
+
'confirm-actions'?: (props: typeof __VLS_74) => any;
|
|
58
62
|
};
|
|
59
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
63
|
+
declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {
|
|
60
64
|
select: (plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any;
|
|
61
|
-
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
65
|
+
}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
62
66
|
onSelect?: ((plan: SubscriptionPlan, price: SubscriptionPlanPrice) => any) | undefined;
|
|
63
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
67
|
+
}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, false, {}, any>;
|
|
64
68
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
65
69
|
declare const _default: typeof __VLS_export;
|
|
66
70
|
export default _default;
|
|
@@ -33,7 +33,7 @@ type __VLS_Slots = {} & {
|
|
|
33
33
|
} & {
|
|
34
34
|
actions?: (props: typeof __VLS_33) => any;
|
|
35
35
|
};
|
|
36
|
-
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>;
|
|
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>;
|
|
37
37
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
38
38
|
declare const _default: typeof __VLS_export;
|
|
39
39
|
export default _default;
|
|
@@ -15,7 +15,8 @@ const planLabel = computed(() => {
|
|
|
15
15
|
});
|
|
16
16
|
const badge = computed(() => {
|
|
17
17
|
const value = subscription.value?.status;
|
|
18
|
-
if (value
|
|
18
|
+
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" };
|
|
19
20
|
if (value === "trialing") return { label: t("trialing", "Trial"), color: "info" };
|
|
20
21
|
if (value === "past_due" || value === "unpaid") return { label: t("pastDue", "Past due"), color: "warning" };
|
|
21
22
|
if (value === "canceled") return { label: t("canceled", "Canceled"), color: "neutral" };
|
|
@@ -24,17 +25,19 @@ const badge = computed(() => {
|
|
|
24
25
|
function formatDate(value) {
|
|
25
26
|
return value ? new Intl.DateTimeFormat(props.locale, { dateStyle: "long" }).format(new Date(value)) : "";
|
|
26
27
|
}
|
|
28
|
+
const isManual = computed(() => subscription.value?.source === "manual");
|
|
27
29
|
const dateText = computed(() => {
|
|
28
|
-
if (cancelsAt.value) return t("cancels", "Ends on {date}", { date: formatDate(cancelsAt.value) });
|
|
29
30
|
const end = subscription.value?.currentPeriodEnd;
|
|
30
|
-
return end ? t("
|
|
31
|
+
if (isManual.value) return end ? t("freeUntil", "Free membership until {date}", { date: formatDate(end) }) : t("free", "Free membership");
|
|
32
|
+
if (cancelsAt.value) return t("cancelsAt", "Ending at {date}", { date: formatDate(cancelsAt.value) });
|
|
33
|
+
return end ? t("renewsAutomaticallyAt", "Renews automatically on {date}", { date: formatDate(end) }) : "";
|
|
31
34
|
});
|
|
32
35
|
const resubscribe = () => subscription.value?.planKey ? subscribe(subscription.value.planKey).catch(() => void 0) : subscribe(subscription.value?.priceId ?? "").catch(() => void 0);
|
|
33
36
|
const manage = () => openPortal().catch(() => void 0);
|
|
34
37
|
</script>
|
|
35
38
|
|
|
36
39
|
<template>
|
|
37
|
-
<UCard>
|
|
40
|
+
<UCard :ui="{ root: 'rounded-xl' }">
|
|
38
41
|
<template #header>
|
|
39
42
|
<slot name="header">
|
|
40
43
|
<h3 class="font-semibold">
|
|
@@ -52,16 +55,22 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
52
55
|
</p>
|
|
53
56
|
</slot>
|
|
54
57
|
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
<div
|
|
59
|
+
v-else-if="isActive && subscription"
|
|
60
|
+
class="flex flex-col gap-4"
|
|
61
|
+
>
|
|
62
|
+
<div class="flex items-center justify-between gap-2">
|
|
63
|
+
<div class="flex flex-col">
|
|
64
|
+
<span class="text-sm text-muted">{{ t("currentPlan", "Current plan") }}</span>
|
|
65
|
+
<slot
|
|
66
|
+
name="plan"
|
|
67
|
+
:plan="plan"
|
|
68
|
+
:label="planLabel"
|
|
69
|
+
:subscription="subscription"
|
|
70
|
+
>
|
|
71
|
+
<span class="text-lg font-semibold">{{ planLabel }}</span>
|
|
72
|
+
</slot>
|
|
73
|
+
</div>
|
|
65
74
|
<slot
|
|
66
75
|
name="badge"
|
|
67
76
|
:subscription="subscription"
|
|
@@ -69,7 +78,8 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
69
78
|
>
|
|
70
79
|
<UBadge
|
|
71
80
|
:color="badge.color"
|
|
72
|
-
|
|
81
|
+
:icon="badge.icon"
|
|
82
|
+
variant="soft"
|
|
73
83
|
>
|
|
74
84
|
{{ badge.label }}
|
|
75
85
|
</UBadge>
|
|
@@ -81,14 +91,14 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
81
91
|
:cancels-at="cancelsAt"
|
|
82
92
|
:date-text="dateText"
|
|
83
93
|
>
|
|
84
|
-
<
|
|
94
|
+
<div
|
|
85
95
|
v-if="dateText"
|
|
86
|
-
class="
|
|
96
|
+
class="rounded-md bg-muted/50 px-4 py-3 text-sm"
|
|
87
97
|
>
|
|
88
98
|
{{ dateText }}
|
|
89
|
-
</
|
|
99
|
+
</div>
|
|
90
100
|
</slot>
|
|
91
|
-
</
|
|
101
|
+
</div>
|
|
92
102
|
|
|
93
103
|
<slot
|
|
94
104
|
v-else
|
|
@@ -108,7 +118,7 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
108
118
|
/>
|
|
109
119
|
|
|
110
120
|
<template
|
|
111
|
-
v-if="isActive"
|
|
121
|
+
v-if="isActive && !isManual"
|
|
112
122
|
#footer
|
|
113
123
|
>
|
|
114
124
|
<slot
|
|
@@ -118,19 +128,30 @@ const manage = () => openPortal().catch(() => void 0);
|
|
|
118
128
|
:cancelling="!!cancelsAt"
|
|
119
129
|
:pending="pending"
|
|
120
130
|
>
|
|
121
|
-
<div class="flex flex-wrap gap-
|
|
131
|
+
<div class="flex flex-wrap gap-3">
|
|
122
132
|
<UButton
|
|
133
|
+
v-if="!cancelsAt"
|
|
134
|
+
color="error"
|
|
135
|
+
variant="soft"
|
|
136
|
+
icon="i-lucide-x"
|
|
123
137
|
:loading="pending"
|
|
124
|
-
:label="t('
|
|
138
|
+
:label="t('cancel', 'Cancel membership')"
|
|
125
139
|
@click="manage"
|
|
126
140
|
/>
|
|
127
141
|
<UButton
|
|
128
|
-
v-
|
|
129
|
-
|
|
142
|
+
v-else
|
|
143
|
+
icon="i-lucide-rotate-ccw"
|
|
130
144
|
:loading="pending"
|
|
131
145
|
:label="t('resubscribe', 'Resubscribe')"
|
|
132
146
|
@click="resubscribe"
|
|
133
147
|
/>
|
|
148
|
+
<UButton
|
|
149
|
+
variant="outline"
|
|
150
|
+
icon="i-lucide-credit-card"
|
|
151
|
+
:loading="pending"
|
|
152
|
+
:label="t('changePaymentMethod', 'Change payment method')"
|
|
153
|
+
@click="manage"
|
|
154
|
+
/>
|
|
134
155
|
</div>
|
|
135
156
|
</slot>
|
|
136
157
|
</template>
|
|
@@ -33,7 +33,7 @@ type __VLS_Slots = {} & {
|
|
|
33
33
|
} & {
|
|
34
34
|
actions?: (props: typeof __VLS_33) => any;
|
|
35
35
|
};
|
|
36
|
-
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>;
|
|
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>;
|
|
37
37
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
38
38
|
declare const _default: typeof __VLS_export;
|
|
39
39
|
export default _default;
|
|
@@ -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;
|