@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.
Files changed (66) 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 +61 -3
  34. package/dist/runtime/i18n/lang/en.json +61 -3
  35. package/dist/runtime/server/db/schema.sqlite.d.ts +19 -0
  36. package/dist/runtime/server/db/schema.sqlite.js +2 -0
  37. package/dist/runtime/server/mcp/tools/my-subscriptions.d.ts +2 -0
  38. package/dist/runtime/server/mcp/tools/my-subscriptions.js +9 -0
  39. package/dist/runtime/server/routes/admin/payment-link.post.d.ts +2 -0
  40. package/dist/runtime/server/routes/admin/payment-link.post.js +46 -0
  41. package/dist/runtime/server/routes/admin/subscription.delete.d.ts +2 -0
  42. package/dist/runtime/server/routes/admin/subscription.delete.js +14 -0
  43. package/dist/runtime/server/routes/admin/subscription.get.d.ts +2 -0
  44. package/dist/runtime/server/routes/admin/subscription.get.js +17 -0
  45. package/dist/runtime/server/routes/admin/subscription.post.d.ts +2 -0
  46. package/dist/runtime/server/routes/admin/subscription.post.js +44 -0
  47. package/dist/runtime/server/routes/admin/subscriptions.get.d.ts +2 -0
  48. package/dist/runtime/server/routes/admin/subscriptions.get.js +4 -0
  49. package/dist/runtime/server/routes/subscriptions/checkout.post.js +16 -30
  50. package/dist/runtime/server/routes/subscriptions/me.get.js +2 -20
  51. package/dist/runtime/server/routes/subscriptions/portal.post.js +2 -2
  52. package/dist/runtime/server/stripe/invoice-events.d.ts +2 -0
  53. package/dist/runtime/server/stripe/invoice-events.js +42 -0
  54. package/dist/runtime/server/utils/admin-membership.d.ts +5 -0
  55. package/dist/runtime/server/utils/admin-membership.js +7 -0
  56. package/dist/runtime/server/utils/checkout.d.ts +15 -0
  57. package/dist/runtime/server/utils/checkout.js +22 -0
  58. package/dist/runtime/server/utils/plans.js +20 -1
  59. package/dist/runtime/server/utils/subscriptions.d.ts +3 -0
  60. package/dist/runtime/server/utils/subscriptions.js +22 -3
  61. package/dist/runtime/utils/index.d.ts +1 -1
  62. package/dist/runtime/utils/index.js +1 -1
  63. package/dist/runtime/utils/plans.d.ts +15 -6
  64. package/dist/runtime/utils/plans.js +16 -11
  65. package/dist/types.d.mts +1 -1
  66. 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 new Intl.NumberFormat(props.locale, {
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
- <div
93
- v-if="visible.length"
94
- class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
95
- >
96
- <UCard
97
- v-for="plan in visible"
98
- :key="plan.key"
99
- :data-plan="plan.key"
100
- >
101
- <template #header>
102
- <slot
103
- name="header"
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
- <template v-if="priceFor(plan)">
142
- <p class="text-2xl font-bold">
143
- {{ formatPrice(priceFor(plan)) }}
144
- <span class="text-sm font-normal text-muted">/ {{ intervalLabel(priceFor(plan)) }}</span>
145
- </p>
146
- <p
147
- v-if="priceFor(plan).trialPeriodDays"
148
- class="mt-1 text-sm text-muted"
149
- >
150
- {{ t("trial", "{days}-day free trial", { days: priceFor(plan).trialPeriodDays }) }}
151
- </p>
152
- </template>
153
- </slot>
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
- <template #footer>
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('confirmTitle', 'Change plan')"
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
- <p>{{ t("confirmBody", "Switch to {plan}? Your subscription will be updated.", { plan: confirming ? labelOf(confirming.plan) : "" }) }}</p>
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
- <UButton
202
- variant="outline"
203
- :label="t('cancel', 'Cancel')"
204
- @click="confirming = null"
205
- />
206
- <UButton
207
- :label="t('confirm', 'Confirm')"
208
- :loading="pending"
209
- @click="confirming && go(confirming.plan, confirming.price)"
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 __VLS_1: {}, __VLS_3: {}, __VLS_17: {
15
- plan: any;
16
- state: "upgrade" | "downgrade" | "subscribe" | "current";
17
- }, __VLS_19: {
18
- plan: any;
19
- state: "upgrade" | "downgrade" | "subscribe" | "current";
20
- }, __VLS_27: {
21
- plan: any;
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
- }, __VLS_29: {
26
- plan: any;
27
- }, __VLS_32: {
28
- plan: any;
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
- }, __VLS_48: {
36
+ }, __VLS_35: {}, __VLS_37: {}, __VLS_66: {
33
37
  plan: any;
34
38
  price: any;
35
- }, __VLS_51: {
39
+ }, __VLS_74: {
36
40
  plan: any;
37
41
  price: any;
38
42
  confirm: () => any;
39
43
  };
40
44
  type __VLS_Slots = {} & {
41
- loading?: (props: typeof __VLS_1) => any;
45
+ header?: (props: typeof __VLS_14) => any;
42
46
  } & {
43
- empty?: (props: typeof __VLS_3) => any;
47
+ badge?: (props: typeof __VLS_17) => any;
44
48
  } & {
45
- header?: (props: typeof __VLS_17) => any;
49
+ price?: (props: typeof __VLS_20) => any;
46
50
  } & {
47
- badge?: (props: typeof __VLS_19) => any;
51
+ features?: (props: typeof __VLS_23) => any;
48
52
  } & {
49
- price?: (props: typeof __VLS_27) => any;
53
+ action?: (props: typeof __VLS_26) => any;
50
54
  } & {
51
- features?: (props: typeof __VLS_29) => any;
55
+ loading?: (props: typeof __VLS_35) => any;
52
56
  } & {
53
- action?: (props: typeof __VLS_32) => any;
57
+ empty?: (props: typeof __VLS_37) => any;
54
58
  } & {
55
- confirm?: (props: typeof __VLS_48) => any;
59
+ confirm?: (props: typeof __VLS_66) => any;
56
60
  } & {
57
- 'confirm-actions'?: (props: typeof __VLS_51) => any;
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 === "active") return { label: t("active", "Active"), color: "success" };
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("renews", "Renews on {date}", { date: formatDate(end) }) : "";
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
- <template v-else-if="isActive && subscription">
56
- <div class="flex flex-wrap items-center gap-2">
57
- <slot
58
- name="plan"
59
- :plan="plan"
60
- :label="planLabel"
61
- :subscription="subscription"
62
- >
63
- <span class="font-medium">{{ planLabel }}</span>
64
- </slot>
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
- variant="subtle"
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
- <p
94
+ <div
85
95
  v-if="dateText"
86
- class="mt-2 text-sm text-muted"
96
+ class="rounded-md bg-muted/50 px-4 py-3 text-sm"
87
97
  >
88
98
  {{ dateText }}
89
- </p>
99
+ </div>
90
100
  </slot>
91
- </template>
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-2">
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('manage', 'Manage billing')"
138
+ :label="t('cancel', 'Cancel membership')"
125
139
  @click="manage"
126
140
  />
127
141
  <UButton
128
- v-if="cancelsAt"
129
- variant="outline"
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;