@frigear-nu/payments 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.
- package/README.md +1 -1
- package/dist/module.d.mts +19 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +37 -4
- package/dist/runtime/app/tabs/Payments.d.vue.ts +8 -0
- package/dist/runtime/app/tabs/Payments.vue +74 -0
- package/dist/runtime/app/tabs/Payments.vue.d.ts +8 -0
- package/dist/runtime/app/widgets/AdminLastPayment.d.vue.ts +8 -0
- package/dist/runtime/app/widgets/AdminLastPayment.vue +33 -0
- package/dist/runtime/app/widgets/AdminLastPayment.vue.d.ts +8 -0
- package/dist/runtime/i18n/lang/da.json +14 -1
- package/dist/runtime/i18n/lang/en.json +14 -1
- package/dist/runtime/server/db/schema.sqlite.d.ts +40 -1
- package/dist/runtime/server/db/schema.sqlite.js +6 -1
- package/dist/runtime/server/mcp/tools/my-payments.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/my-payments.js +8 -0
- package/dist/runtime/server/plugins/invoice-payments.d.ts +2 -0
- package/dist/runtime/server/plugins/invoice-payments.js +7 -0
- package/dist/runtime/server/routes/admin/user-payments.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/user-payments.get.js +5 -0
- package/dist/runtime/server/routes/payments/index.get.js +1 -1
- package/dist/runtime/server/routes/payments/resume.post.js +1 -1
- package/dist/runtime/server/utils/payments.d.ts +15 -3
- package/dist/runtime/server/utils/payments.js +101 -48
- package/dist/runtime/utils/index.d.ts +0 -2
- package/dist/runtime/utils/index.js +0 -11
- package/dist/types.d.mts +1 -1
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ const { payment, url } = await createPayment({
|
|
|
33
33
|
nitroApp.hooks.hook('payments:paid', ({ payment }) => { /* ... */ })
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Full documentation: `apps/docs/content/4.modules/
|
|
36
|
+
Full documentation: `apps/docs/content/4.modules/13.payments.md`.
|
|
37
37
|
|
|
38
38
|
## Development
|
|
39
39
|
|
package/dist/module.d.mts
CHANGED
|
@@ -6,6 +6,7 @@ type ModuleOptions = {
|
|
|
6
6
|
settings?: {
|
|
7
7
|
/** Minutes a Checkout session stays open before it expires (Stripe allows 30 - 1440). Defaults to `30`. */
|
|
8
8
|
checkoutExpiresInMinutes?: number;
|
|
9
|
+
mcp?: boolean;
|
|
9
10
|
};
|
|
10
11
|
};
|
|
11
12
|
type PaymentsRuntimeConfig = {
|
|
@@ -54,6 +55,23 @@ type CreatePaymentResult = {
|
|
|
54
55
|
payment: PaymentDto;
|
|
55
56
|
url: string;
|
|
56
57
|
};
|
|
58
|
+
/** An invoice to store as a payment, see `recordInvoicePayment`. */
|
|
59
|
+
type InvoicePaymentInput = {
|
|
60
|
+
userId: number;
|
|
61
|
+
invoiceId: string;
|
|
62
|
+
status: 'paid' | 'failed' | 'expired';
|
|
63
|
+
/** Total in the currency's minor unit. */
|
|
64
|
+
amount: number;
|
|
65
|
+
currency: string;
|
|
66
|
+
description: string | null;
|
|
67
|
+
/** Defaults to `subscription`. */
|
|
68
|
+
referenceType?: string;
|
|
69
|
+
/** What the invoice is for, e.g. the Stripe subscription id. */
|
|
70
|
+
referenceId: string | null;
|
|
71
|
+
paymentIntentId: string | null;
|
|
72
|
+
paidAt: Date | null;
|
|
73
|
+
createdAt?: Date;
|
|
74
|
+
};
|
|
57
75
|
type PaymentHookContext = {
|
|
58
76
|
payment: PaymentDto;
|
|
59
77
|
};
|
|
@@ -93,4 +111,4 @@ declare module 'nitropack' {
|
|
|
93
111
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
94
112
|
|
|
95
113
|
export { _default as default };
|
|
96
|
-
export type { CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, PaymentsRuntimeConfig, ResumeResponse };
|
|
114
|
+
export type { CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, InvoicePaymentInput, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, PaymentsRuntimeConfig, ResumeResponse };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addImports } from '@nuxt/kit';
|
|
2
|
-
import { registerModule, registerLocales, registerDatabaseTable, moduleApiUrl } from '@frigear-nu/module-kit/utils';
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addServerPlugin, addImports } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, registerDatabaseTable, moduleApiUrl } from '@frigear-nu/module-kit/utils';
|
|
3
|
+
import { addAuthScope } from '@frigear-nu/auth/utils';
|
|
4
|
+
import { extendUsersAdmin } from '@frigear-nu/users/utils';
|
|
5
|
+
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
3
6
|
import { registerStripeEventHandler } from '@frigear-nu/stripe/utils';
|
|
4
7
|
|
|
5
8
|
const MODULE_NAME = "@frigear-nu/payments";
|
|
6
9
|
const module$1 = defineNuxtModule({
|
|
7
10
|
meta: { name: MODULE_NAME, configKey: "payments" },
|
|
8
11
|
defaults: {
|
|
9
|
-
settings: { checkoutExpiresInMinutes: 30 }
|
|
12
|
+
settings: { checkoutExpiresInMinutes: 30, mcp: false }
|
|
10
13
|
},
|
|
11
14
|
moduleDependencies: {
|
|
12
15
|
"@nuxthub/core": {
|
|
@@ -14,12 +17,23 @@ const module$1 = defineNuxtModule({
|
|
|
14
17
|
defaults: { kv: true }
|
|
15
18
|
},
|
|
16
19
|
"@frigear-nu/module-kit": { version: ">=0.0.1" },
|
|
20
|
+
"@frigear-nu/auth": { version: ">=0.0.1" },
|
|
17
21
|
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
18
|
-
"@frigear-nu/stripe": { version: ">=0.0.1" }
|
|
22
|
+
"@frigear-nu/stripe": { version: ">=0.0.1" },
|
|
23
|
+
"@frigear-nu/mcp": { version: ">=0.0.1" },
|
|
24
|
+
"@frigear-nu/widget": { version: ">=0.0.0", optional: true },
|
|
25
|
+
// The admin user tab and card are built from its components, so they are only added with it.
|
|
26
|
+
"@frigear-nu/admin": { version: ">=0.0.0", optional: true }
|
|
19
27
|
},
|
|
20
28
|
async setup(options, nuxt) {
|
|
21
29
|
registerModule(MODULE_NAME, nuxt);
|
|
30
|
+
addAuthScope(
|
|
31
|
+
nuxt,
|
|
32
|
+
{ name: "payments:read", description: "Read your payments" },
|
|
33
|
+
{ name: "payments:write", description: "Resume your payments" }
|
|
34
|
+
);
|
|
22
35
|
const resolver = createResolver(import.meta.url);
|
|
36
|
+
registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
|
|
23
37
|
registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
|
|
24
38
|
nuxt.options.runtimeConfig.payments = {
|
|
25
39
|
checkoutExpiresInMinutes: options.settings?.checkoutExpiresInMinutes ?? 30
|
|
@@ -49,7 +63,26 @@ const module$1 = defineNuxtModule({
|
|
|
49
63
|
method: "post",
|
|
50
64
|
handler: resolver.resolve("./runtime/server/routes/payments/resume.post")
|
|
51
65
|
});
|
|
66
|
+
await extendUsersAdmin(nuxt, {
|
|
67
|
+
routes: [{ method: "get", path: "payments", handler: resolver.resolve("./runtime/server/routes/admin/user-payments.get") }],
|
|
68
|
+
tab: {
|
|
69
|
+
id: "payments",
|
|
70
|
+
component: resolver.resolve("./runtime/app/tabs/Payments.vue"),
|
|
71
|
+
order: 20,
|
|
72
|
+
label: "Payments",
|
|
73
|
+
labelKey: "payments.admin.tab",
|
|
74
|
+
icon: "i-lucide-credit-card"
|
|
75
|
+
},
|
|
76
|
+
widget: {
|
|
77
|
+
id: "payments-last",
|
|
78
|
+
component: resolver.resolve("./runtime/app/widgets/AdminLastPayment.vue"),
|
|
79
|
+
order: 20,
|
|
80
|
+
span: 2
|
|
81
|
+
}
|
|
82
|
+
});
|
|
52
83
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
84
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/invoice-payments"));
|
|
85
|
+
if (options.settings?.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
53
86
|
addImports([
|
|
54
87
|
{ name: "usePayments", from: resolver.resolve("./runtime/app/composables/usePayments") }
|
|
55
88
|
]);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
user: {
|
|
3
|
+
id: number;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { formatMoney as formatMinorAmount } from "@frigear-nu/ui/money";
|
|
3
|
+
defineOptions({ name: "UsersAdminPayments" });
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
user: { type: Object, required: true }
|
|
6
|
+
});
|
|
7
|
+
const t = useModuleT("payments.admin");
|
|
8
|
+
const status = useModuleT("payments.status");
|
|
9
|
+
const { data: payments, status: fetchStatus } = useLazyFetch(
|
|
10
|
+
() => `/api/admin/users/${props.user.id}/payments`,
|
|
11
|
+
{ key: () => `admin-payments-${props.user.id}` }
|
|
12
|
+
);
|
|
13
|
+
const statusColors = {
|
|
14
|
+
paid: "success",
|
|
15
|
+
pending: "warning",
|
|
16
|
+
failed: "error",
|
|
17
|
+
expired: "neutral",
|
|
18
|
+
refunded: "neutral",
|
|
19
|
+
partially_refunded: "warning"
|
|
20
|
+
};
|
|
21
|
+
const statusLabel = (value) => status(value, value);
|
|
22
|
+
const columns = computed(() => [
|
|
23
|
+
{ accessorKey: "createdAt", header: t("date", "Date") },
|
|
24
|
+
{ accessorKey: "description", header: t("description", "Description") },
|
|
25
|
+
{ accessorKey: "amount", header: t("amount", "Amount") },
|
|
26
|
+
{ accessorKey: "status", header: t("statusColumn", "Status") },
|
|
27
|
+
{ accessorKey: "refundedAmount", header: t("refunded", "Refunded") },
|
|
28
|
+
{ accessorKey: "referenceType", header: t("reference", "Reference") }
|
|
29
|
+
]);
|
|
30
|
+
const date = (value) => value ? new Date(value).toLocaleString() : "\u2013";
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<AdminSection
|
|
35
|
+
:title="t('title', 'Payments')"
|
|
36
|
+
>
|
|
37
|
+
<template #flush>
|
|
38
|
+
<UTable
|
|
39
|
+
:data="payments ?? []"
|
|
40
|
+
:columns="columns"
|
|
41
|
+
:loading="fetchStatus === 'pending'"
|
|
42
|
+
>
|
|
43
|
+
<template #createdAt-cell="{ row }">
|
|
44
|
+
<span class="whitespace-nowrap">{{ date(row.original.paidAt ?? row.original.createdAt) }}</span>
|
|
45
|
+
</template>
|
|
46
|
+
<template #description-cell="{ row }">
|
|
47
|
+
{{ row.original.description ?? t("defaultDescription", "Payment") }}
|
|
48
|
+
</template>
|
|
49
|
+
<template #amount-cell="{ row }">
|
|
50
|
+
<span class="whitespace-nowrap">{{ formatMinorAmount(row.original.amount, row.original.currency) }}</span>
|
|
51
|
+
</template>
|
|
52
|
+
<template #status-cell="{ row }">
|
|
53
|
+
<UBadge
|
|
54
|
+
variant="subtle"
|
|
55
|
+
:color="statusColors[row.original.status] ?? 'neutral'"
|
|
56
|
+
:label="statusLabel(row.original.status)"
|
|
57
|
+
/>
|
|
58
|
+
</template>
|
|
59
|
+
<template #refundedAmount-cell="{ row }">
|
|
60
|
+
<span class="whitespace-nowrap">{{ row.original.refundedAmount ? formatMinorAmount(row.original.refundedAmount, row.original.currency) : "\u2013" }}</span>
|
|
61
|
+
</template>
|
|
62
|
+
<template #referenceType-cell="{ row }">
|
|
63
|
+
<span class="text-muted">{{ row.original.referenceType ? `${row.original.referenceType} ${row.original.referenceId ?? ""}`.trim() : "\u2013" }}</span>
|
|
64
|
+
</template>
|
|
65
|
+
<template #empty>
|
|
66
|
+
<AdminEmpty
|
|
67
|
+
icon="i-lucide-credit-card"
|
|
68
|
+
:title="t('empty', 'No payments yet.')"
|
|
69
|
+
/>
|
|
70
|
+
</template>
|
|
71
|
+
</UTable>
|
|
72
|
+
</template>
|
|
73
|
+
</AdminSection>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
user: {
|
|
3
|
+
id: number;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
user: {
|
|
3
|
+
id: number;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { formatMoney as formatMinorAmount } from "@frigear-nu/ui/money";
|
|
3
|
+
defineOptions({ name: "UsersAdminLastPayment" });
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
user: { type: Object, required: true }
|
|
6
|
+
});
|
|
7
|
+
const t = useModuleT("payments.admin");
|
|
8
|
+
const status = useModuleT("payments.status");
|
|
9
|
+
const { data: payments, status: fetchStatus } = useLazyFetch(
|
|
10
|
+
() => `/api/admin/users/${props.user.id}/payments`,
|
|
11
|
+
{ key: () => `admin-last-payment-${props.user.id}`, query: { limit: 1 } }
|
|
12
|
+
);
|
|
13
|
+
const last = computed(() => payments.value?.[0]);
|
|
14
|
+
const date = (value) => value ? new Date(value).toLocaleDateString() : "\u2013";
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<AdminStatCard
|
|
19
|
+
:label="t('lastPayment', 'Last payment')"
|
|
20
|
+
:empty="t('empty', 'No payments yet.')"
|
|
21
|
+
:loading="fetchStatus === 'pending' && !payments"
|
|
22
|
+
>
|
|
23
|
+
<template v-if="last">
|
|
24
|
+
<span class="text-highlighted font-medium">{{ formatMinorAmount(last.amount, last.currency) }}</span>
|
|
25
|
+
<UBadge
|
|
26
|
+
variant="subtle"
|
|
27
|
+
:color="last.status === 'paid' ? 'success' : 'neutral'"
|
|
28
|
+
:label="status(last.status, last.status)"
|
|
29
|
+
/>
|
|
30
|
+
<span class="text-muted">{{ date(last.paidAt ?? last.createdAt) }}</span>
|
|
31
|
+
</template>
|
|
32
|
+
</AdminStatCard>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
user: {
|
|
3
|
+
id: number;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
"refunded": "Refunderet",
|
|
9
9
|
"partially_refunded": "Delvist refunderet"
|
|
10
10
|
},
|
|
11
|
-
"defaultDescription": "Betaling"
|
|
11
|
+
"defaultDescription": "Betaling",
|
|
12
|
+
"admin": {
|
|
13
|
+
"tab": "Betalinger",
|
|
14
|
+
"title": "Betalinger",
|
|
15
|
+
"lastPayment": "Seneste betaling",
|
|
16
|
+
"empty": "Ingen betalinger endnu.",
|
|
17
|
+
"date": "Dato",
|
|
18
|
+
"description": "Beskrivelse",
|
|
19
|
+
"amount": "Beløb",
|
|
20
|
+
"statusColumn": "Status",
|
|
21
|
+
"refunded": "Refunderet",
|
|
22
|
+
"reference": "Reference",
|
|
23
|
+
"defaultDescription": "Betaling"
|
|
24
|
+
}
|
|
12
25
|
}
|
|
13
26
|
}
|
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
"refunded": "Refunded",
|
|
9
9
|
"partially_refunded": "Partially refunded"
|
|
10
10
|
},
|
|
11
|
-
"defaultDescription": "Payment"
|
|
11
|
+
"defaultDescription": "Payment",
|
|
12
|
+
"admin": {
|
|
13
|
+
"tab": "Payments",
|
|
14
|
+
"title": "Payments",
|
|
15
|
+
"lastPayment": "Last payment",
|
|
16
|
+
"empty": "No payments yet.",
|
|
17
|
+
"date": "Date",
|
|
18
|
+
"description": "Description",
|
|
19
|
+
"amount": "Amount",
|
|
20
|
+
"statusColumn": "Status",
|
|
21
|
+
"refunded": "Refunded",
|
|
22
|
+
"reference": "Reference",
|
|
23
|
+
"defaultDescription": "Payment"
|
|
24
|
+
}
|
|
12
25
|
}
|
|
13
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PaymentItem } from '../../../types.js';
|
|
1
2
|
export declare const payments: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
2
3
|
name: "payments";
|
|
3
4
|
schema: undefined;
|
|
@@ -157,7 +158,26 @@ export declare const payments: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
157
158
|
columnType: "SQLiteText";
|
|
158
159
|
data: string;
|
|
159
160
|
driverParam: string;
|
|
160
|
-
notNull:
|
|
161
|
+
notNull: false;
|
|
162
|
+
hasDefault: false;
|
|
163
|
+
isPrimaryKey: false;
|
|
164
|
+
isAutoincrement: false;
|
|
165
|
+
hasRuntimeDefault: false;
|
|
166
|
+
enumValues: [string, ...string[]];
|
|
167
|
+
baseColumn: never;
|
|
168
|
+
identity: undefined;
|
|
169
|
+
generated: undefined;
|
|
170
|
+
}, {}, {
|
|
171
|
+
length: undefined;
|
|
172
|
+
}>;
|
|
173
|
+
invoiceId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
174
|
+
name: "invoiceId";
|
|
175
|
+
tableName: "payments";
|
|
176
|
+
dataType: "string";
|
|
177
|
+
columnType: "SQLiteText";
|
|
178
|
+
data: string;
|
|
179
|
+
driverParam: string;
|
|
180
|
+
notNull: false;
|
|
161
181
|
hasDefault: false;
|
|
162
182
|
isPrimaryKey: false;
|
|
163
183
|
isAutoincrement: false;
|
|
@@ -224,6 +244,25 @@ export declare const payments: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
224
244
|
}, {}, {
|
|
225
245
|
$type: Record<string, string>;
|
|
226
246
|
}>;
|
|
247
|
+
items: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
248
|
+
name: "items";
|
|
249
|
+
tableName: "payments";
|
|
250
|
+
dataType: "json";
|
|
251
|
+
columnType: "SQLiteTextJson";
|
|
252
|
+
data: PaymentItem[];
|
|
253
|
+
driverParam: string;
|
|
254
|
+
notNull: false;
|
|
255
|
+
hasDefault: false;
|
|
256
|
+
isPrimaryKey: false;
|
|
257
|
+
isAutoincrement: false;
|
|
258
|
+
hasRuntimeDefault: false;
|
|
259
|
+
enumValues: undefined;
|
|
260
|
+
baseColumn: never;
|
|
261
|
+
identity: undefined;
|
|
262
|
+
generated: undefined;
|
|
263
|
+
}, {}, {
|
|
264
|
+
$type: PaymentItem[];
|
|
265
|
+
}>;
|
|
227
266
|
paidAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
228
267
|
name: "paidAt";
|
|
229
268
|
tableName: "payments";
|
|
@@ -12,10 +12,15 @@ export const payments = sqliteTable("payments", {
|
|
|
12
12
|
/** What the payment is for (e.g. `event_ticket`) and the id of that thing. */
|
|
13
13
|
referenceType: text(),
|
|
14
14
|
referenceId: text(),
|
|
15
|
-
|
|
15
|
+
/** `null` for payments recorded from a Stripe invoice (e.g. a membership renewal), which have no Checkout session. */
|
|
16
|
+
checkoutSessionId: text().unique(),
|
|
17
|
+
/** Stripe invoice id for payments recorded from an invoice (see `recordInvoicePayment`). */
|
|
18
|
+
invoiceId: text().unique(),
|
|
16
19
|
paymentIntentId: text(),
|
|
17
20
|
refundedAmount: integer().notNull().default(0),
|
|
18
21
|
metadata: text({ mode: "json" }).$type(),
|
|
22
|
+
/** The line items the session was created from, so an expired session can be rebuilt. `null` for rows created before this column existed. */
|
|
23
|
+
items: text({ mode: "json" }).$type(),
|
|
19
24
|
paidAt: integer({ mode: "timestamp" }),
|
|
20
25
|
createdAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
21
26
|
updatedAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export default defineScopedMcpTool({
|
|
3
|
+
description: "List your own payments, newest first: tickets, memberships and other charges with amount, status, refunds and what each was for.",
|
|
4
|
+
scopes: ["payments:read"],
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: { limit: z.number().int().min(1).max(100).optional().describe("How many payments to return (default 50).") },
|
|
7
|
+
handler: async ({ limit }, { user }) => ({ payments: await listUserPayments(user.id, { limit: limit ?? 50 }) })
|
|
8
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
const { target } = await loadUsersAdminTarget(event, "view");
|
|
3
|
+
const limit = Number(getQuery(event).limit);
|
|
4
|
+
return listUserPayments(target.id, { limit: Number.isInteger(limit) && limit > 0 ? Math.min(limit, 500) : 100 });
|
|
5
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const bodySchema = z.object({ paymentId: z.string().min(1) });
|
|
3
3
|
export default defineEventHandler(async (event) => {
|
|
4
|
-
const
|
|
4
|
+
const user = await requireUser(event, { scopes: ["payments:write"] });
|
|
5
5
|
const { paymentId } = await readValidatedBody(event, bodySchema.parse);
|
|
6
6
|
return resumePayment(paymentId, user.id, event);
|
|
7
7
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { schema } from '@nuxthub/db';
|
|
2
2
|
import type { H3Event } from 'h3';
|
|
3
|
-
import type { CreatePaymentInput, CreatePaymentResult, PaymentDto } from '../../../types.js';
|
|
3
|
+
import type { CreatePaymentInput, InvoicePaymentInput, CreatePaymentResult, PaymentDto } from '../../../types.js';
|
|
4
4
|
type PaymentRow = typeof schema.payments.$inferSelect;
|
|
5
5
|
export declare function toPaymentDto(row: PaymentRow): PaymentDto;
|
|
6
6
|
/**
|
|
@@ -12,10 +12,14 @@ export declare function toPaymentDto(row: PaymentRow): PaymentDto;
|
|
|
12
12
|
export declare function createPayment(input: CreatePaymentInput, event?: H3Event): Promise<CreatePaymentResult>;
|
|
13
13
|
export declare function getPayment(id: string): Promise<PaymentDto | null>;
|
|
14
14
|
export declare function getPaymentByReference(type: string, id: string): Promise<PaymentDto | null>;
|
|
15
|
-
|
|
15
|
+
/** A user's payments, newest first; `limit` caps how many are returned. */
|
|
16
|
+
export declare function listUserPayments(userId: number, options?: {
|
|
17
|
+
limit?: number;
|
|
18
|
+
}): Promise<PaymentDto[]>;
|
|
16
19
|
/**
|
|
17
20
|
* Returns a checkout URL for a still-unpaid payment: the original session when it is open, otherwise
|
|
18
|
-
* a fresh session
|
|
21
|
+
* a fresh session built from the stored items (the payment row follows the new session). The
|
|
22
|
+
* `payments:checkout:before` hook and return URL validation run again, as in `createPayment`. Throws for payments
|
|
19
23
|
* that are already paid/refunded.
|
|
20
24
|
*/
|
|
21
25
|
export declare function resumePayment(paymentId: string, userId: number, event?: H3Event): Promise<{
|
|
@@ -28,4 +32,12 @@ export declare function resumePayment(paymentId: string, userId: number, event?:
|
|
|
28
32
|
export declare function refundPayment(paymentId: string, options?: {
|
|
29
33
|
amount?: number;
|
|
30
34
|
}): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Stores (or updates) the payment for a Stripe invoice, keyed by the invoice id, so recurring charges such as
|
|
37
|
+
* membership renewals show up next to one-time payments. Idempotent: a redelivered event changes nothing and a
|
|
38
|
+
* late `failed`/`expired` never overwrites a payment that is already paid or refunded. Fires the matching
|
|
39
|
+
* `payments:*` hook only when the status actually changed. Refunds arrive through the `charge.refunded` handler,
|
|
40
|
+
* which finds the row by `paymentIntentId`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function recordInvoicePayment(input: InvoicePaymentInput): Promise<PaymentDto>;
|
|
31
43
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db, schema } from "@nuxthub/db";
|
|
2
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
3
|
-
import { resolveSameOriginUrl
|
|
2
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { resolveSameOriginUrl } from "@frigear-nu/stripe/utils";
|
|
4
|
+
import { sumItems } from "../../utils/index.js";
|
|
4
5
|
export function toPaymentDto(row) {
|
|
5
6
|
return {
|
|
6
7
|
id: row.id,
|
|
@@ -18,11 +19,46 @@ export function toPaymentDto(row) {
|
|
|
18
19
|
}
|
|
19
20
|
const newPaymentId = () => `pay_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
20
21
|
function safeReturnUrl(input, event, name) {
|
|
21
|
-
if (!event)
|
|
22
|
+
if (!event) {
|
|
23
|
+
const absolute = URL.canParse(input) && ["http:", "https:"].includes(new URL(input).protocol);
|
|
24
|
+
if (!absolute) throw createError({ statusCode: 400, statusMessage: `${name} must be an absolute http(s) URL.` });
|
|
25
|
+
return input;
|
|
26
|
+
}
|
|
22
27
|
const resolved = resolveSameOriginUrl(input, getRequestURL(event).origin);
|
|
23
28
|
if (!resolved) throw createError({ statusCode: 400, statusMessage: `${name} must be a relative path or an URL on the same origin.` });
|
|
24
29
|
return resolved;
|
|
25
30
|
}
|
|
31
|
+
function toLineItems(items, currency) {
|
|
32
|
+
return items.map((item) => ({
|
|
33
|
+
quantity: item.quantity ?? 1,
|
|
34
|
+
price_data: {
|
|
35
|
+
currency,
|
|
36
|
+
unit_amount: item.amount,
|
|
37
|
+
product_data: { name: item.name, ...item.description ? { description: item.description } : {} }
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
async function buildCheckoutParams(source) {
|
|
42
|
+
const metadata = { ...source.metadata, paymentId: source.paymentId, userId: String(source.userId) };
|
|
43
|
+
const context = {
|
|
44
|
+
event: source.event,
|
|
45
|
+
userId: source.userId,
|
|
46
|
+
reference: source.reference,
|
|
47
|
+
params: {
|
|
48
|
+
mode: "payment",
|
|
49
|
+
customer: source.customer,
|
|
50
|
+
client_reference_id: String(source.userId),
|
|
51
|
+
line_items: toLineItems(source.items, source.currency),
|
|
52
|
+
success_url: safeReturnUrl(source.successUrl, source.event, "successUrl"),
|
|
53
|
+
cancel_url: safeReturnUrl(source.cancelUrl, source.event, "cancelUrl"),
|
|
54
|
+
expires_at: expiresAt(source.expiresInMinutes),
|
|
55
|
+
metadata,
|
|
56
|
+
payment_intent_data: { metadata }
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
await useNitroApp().hooks.callHook("payments:checkout:before", context);
|
|
60
|
+
return context.params;
|
|
61
|
+
}
|
|
26
62
|
function expiresAt(minutes) {
|
|
27
63
|
const clamped = Math.min(Math.max(Math.round(minutes), 30), 1440);
|
|
28
64
|
return Math.floor(Date.now() / 1e3) + clamped * 60;
|
|
@@ -39,32 +75,20 @@ export async function createPayment(input, event) {
|
|
|
39
75
|
const stripe = useStripe();
|
|
40
76
|
const customer = await ensureStripeCustomer(input.userId);
|
|
41
77
|
const id = newPaymentId();
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
event,
|
|
78
|
+
const params = await buildCheckoutParams({
|
|
79
|
+
paymentId: id,
|
|
45
80
|
userId: input.userId,
|
|
81
|
+
customer,
|
|
82
|
+
currency,
|
|
83
|
+
items: input.items,
|
|
46
84
|
reference: input.reference,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
currency,
|
|
55
|
-
unit_amount: item.amount,
|
|
56
|
-
product_data: { name: item.name, ...item.description ? { description: item.description } : {} }
|
|
57
|
-
}
|
|
58
|
-
})),
|
|
59
|
-
success_url: safeReturnUrl(input.successUrl, event, "successUrl"),
|
|
60
|
-
cancel_url: safeReturnUrl(input.cancelUrl, event, "cancelUrl"),
|
|
61
|
-
expires_at: expiresAt(input.expiresInMinutes ?? config.checkoutExpiresInMinutes),
|
|
62
|
-
metadata,
|
|
63
|
-
payment_intent_data: { metadata }
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
await useNitroApp().hooks.callHook("payments:checkout:before", context);
|
|
67
|
-
const session = await stripe.checkout.sessions.create(context.params);
|
|
85
|
+
successUrl: input.successUrl,
|
|
86
|
+
cancelUrl: input.cancelUrl,
|
|
87
|
+
expiresInMinutes: input.expiresInMinutes ?? config.checkoutExpiresInMinutes,
|
|
88
|
+
metadata: input.metadata,
|
|
89
|
+
event
|
|
90
|
+
});
|
|
91
|
+
const session = await stripe.checkout.sessions.create(params);
|
|
68
92
|
if (!session.url) throw createError({ statusCode: 502, statusMessage: "Stripe did not return a checkout URL." });
|
|
69
93
|
try {
|
|
70
94
|
const [row] = await db.insert(schema.payments).values({
|
|
@@ -76,7 +100,8 @@ export async function createPayment(input, event) {
|
|
|
76
100
|
referenceType: input.reference?.type ?? null,
|
|
77
101
|
referenceId: input.reference?.id ?? null,
|
|
78
102
|
checkoutSessionId: session.id,
|
|
79
|
-
metadata: input.metadata ?? null
|
|
103
|
+
metadata: input.metadata ?? null,
|
|
104
|
+
items: input.items
|
|
80
105
|
}).returning();
|
|
81
106
|
return { payment: toPaymentDto(row), url: session.url };
|
|
82
107
|
} catch (error) {
|
|
@@ -92,8 +117,9 @@ export async function getPaymentByReference(type, id) {
|
|
|
92
117
|
const [row] = await db.select().from(schema.payments).where(and(eq(schema.payments.referenceType, type), eq(schema.payments.referenceId, id))).orderBy(desc(schema.payments.createdAt)).limit(1);
|
|
93
118
|
return row ? toPaymentDto(row) : null;
|
|
94
119
|
}
|
|
95
|
-
export async function listUserPayments(userId) {
|
|
96
|
-
const
|
|
120
|
+
export async function listUserPayments(userId, options = {}) {
|
|
121
|
+
const query = db.select().from(schema.payments).where(eq(schema.payments.userId, userId)).orderBy(desc(sql`coalesce(${schema.payments.paidAt}, ${schema.payments.createdAt})`));
|
|
122
|
+
const rows = await (options.limit ? query.limit(options.limit) : query);
|
|
97
123
|
return rows.map(toPaymentDto);
|
|
98
124
|
}
|
|
99
125
|
export async function resumePayment(paymentId, userId, event) {
|
|
@@ -103,33 +129,30 @@ export async function resumePayment(paymentId, userId, event) {
|
|
|
103
129
|
throw createError({ statusCode: 409, statusMessage: "This payment is already paid." });
|
|
104
130
|
}
|
|
105
131
|
const stripe = useStripe();
|
|
132
|
+
if (!row.checkoutSessionId) throw createError({ statusCode: 409, statusMessage: "This payment cannot be resumed." });
|
|
106
133
|
const session = await stripe.checkout.sessions.retrieve(row.checkoutSessionId);
|
|
107
134
|
if (session.status === "open" && session.url) return { url: session.url };
|
|
108
135
|
if (session.status === "complete") throw createError({ statusCode: 409, statusMessage: "This payment is already paid." });
|
|
109
|
-
const customer = await ensureStripeCustomer(userId);
|
|
110
|
-
const metadata = { ...row.metadata, paymentId: row.id, userId: String(userId) };
|
|
111
136
|
const origin = event ? getRequestURL(event).origin : void 0;
|
|
112
137
|
const successUrl = session.success_url ?? origin;
|
|
113
138
|
const cancelUrl = session.cancel_url ?? origin;
|
|
114
139
|
if (!successUrl || !cancelUrl) throw createError({ statusCode: 409, statusMessage: "Cannot resume this payment." });
|
|
115
|
-
const
|
|
116
|
-
|
|
140
|
+
const items = row.items?.length ? row.items : [{ name: row.description ?? "Payment", amount: row.amount, currency: row.currency }];
|
|
141
|
+
const customer = await ensureStripeCustomer(userId);
|
|
142
|
+
const params = await buildCheckoutParams({
|
|
143
|
+
paymentId: row.id,
|
|
144
|
+
userId,
|
|
117
145
|
customer,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}],
|
|
127
|
-
success_url: successUrl,
|
|
128
|
-
cancel_url: cancelUrl,
|
|
129
|
-
expires_at: expiresAt(useRuntimeConfig().payments.checkoutExpiresInMinutes),
|
|
130
|
-
metadata,
|
|
131
|
-
payment_intent_data: { metadata }
|
|
146
|
+
currency: row.currency,
|
|
147
|
+
items,
|
|
148
|
+
reference: row.referenceType && row.referenceId ? { type: row.referenceType, id: row.referenceId } : void 0,
|
|
149
|
+
successUrl,
|
|
150
|
+
cancelUrl,
|
|
151
|
+
expiresInMinutes: useRuntimeConfig().payments.checkoutExpiresInMinutes,
|
|
152
|
+
metadata: row.metadata,
|
|
153
|
+
event
|
|
132
154
|
});
|
|
155
|
+
const next = await stripe.checkout.sessions.create(params);
|
|
133
156
|
if (!next.url) throw createError({ statusCode: 502, statusMessage: "Stripe did not return a checkout URL." });
|
|
134
157
|
await db.update(schema.payments).set({ checkoutSessionId: next.id, status: "pending" }).where(eq(schema.payments.id, row.id));
|
|
135
158
|
return { url: next.url };
|
|
@@ -148,3 +171,33 @@ export async function refundPayment(paymentId, options = {}) {
|
|
|
148
171
|
...options.amount !== void 0 ? { amount: options.amount } : {}
|
|
149
172
|
});
|
|
150
173
|
}
|
|
174
|
+
const hookForInvoice = { paid: "payments:paid", failed: "payments:failed", expired: "payments:expired" };
|
|
175
|
+
export async function recordInvoicePayment(input) {
|
|
176
|
+
const nitro = useNitroApp();
|
|
177
|
+
const [existing] = await db.select().from(schema.payments).where(eq(schema.payments.invoiceId, input.invoiceId)).limit(1);
|
|
178
|
+
if (existing) {
|
|
179
|
+
const settled = ["paid", "refunded", "partially_refunded"].includes(existing.status);
|
|
180
|
+
if (settled || existing.status === input.status) return toPaymentDto(existing);
|
|
181
|
+
}
|
|
182
|
+
const values = {
|
|
183
|
+
userId: input.userId,
|
|
184
|
+
status: input.status,
|
|
185
|
+
amount: input.amount,
|
|
186
|
+
currency: input.currency,
|
|
187
|
+
description: input.description,
|
|
188
|
+
referenceType: input.referenceType ?? "subscription",
|
|
189
|
+
referenceId: input.referenceId,
|
|
190
|
+
invoiceId: input.invoiceId,
|
|
191
|
+
paymentIntentId: input.paymentIntentId,
|
|
192
|
+
paidAt: input.status === "paid" ? input.paidAt ?? /* @__PURE__ */ new Date() : null
|
|
193
|
+
};
|
|
194
|
+
const [row] = existing ? await db.update(schema.payments).set(values).where(eq(schema.payments.id, existing.id)).returning() : await db.insert(schema.payments).values({ id: newPaymentId(), ...values, ...input.createdAt ? { createdAt: input.createdAt } : {} }).onConflictDoNothing({ target: schema.payments.invoiceId }).returning();
|
|
195
|
+
if (!row) {
|
|
196
|
+
const [current] = await db.select().from(schema.payments).where(eq(schema.payments.invoiceId, input.invoiceId)).limit(1);
|
|
197
|
+
if (!current) throw createError({ statusCode: 500, statusMessage: "Could not record the invoice payment." });
|
|
198
|
+
return toPaymentDto(current);
|
|
199
|
+
}
|
|
200
|
+
const dto = toPaymentDto(row);
|
|
201
|
+
if (input.status in hookForInvoice) await nitro.hooks.callHook(hookForInvoice[input.status], { payment: dto });
|
|
202
|
+
return dto;
|
|
203
|
+
}
|
|
@@ -2,5 +2,3 @@ import type { PaymentItem } from '../../types.js';
|
|
|
2
2
|
export type { CheckoutBeforeContext, CreatePaymentInput, CreatePaymentResult, ModuleOptions, PaymentDto, PaymentHookContext, PaymentItem, PaymentReference, PaymentStatus, } from '../../types.js';
|
|
3
3
|
/** Total of a list of items in minor units. */
|
|
4
4
|
export declare function sumItems(items: PaymentItem[]): number;
|
|
5
|
-
/** Resolves a relative path against `origin`, and only accepts absolute URLs on the same origin. */
|
|
6
|
-
export declare function resolveSameOriginUrl(input: string, origin: string): string | null;
|
|
@@ -1,14 +1,3 @@
|
|
|
1
1
|
export function sumItems(items) {
|
|
2
2
|
return items.reduce((total, item) => total + item.amount * (item.quantity ?? 1), 0);
|
|
3
3
|
}
|
|
4
|
-
export function resolveSameOriginUrl(input, origin) {
|
|
5
|
-
if (input.startsWith("/")) {
|
|
6
|
-
if (input.startsWith("//") || input.startsWith("/\\")) return null;
|
|
7
|
-
return `${origin}${input}`;
|
|
8
|
-
}
|
|
9
|
-
try {
|
|
10
|
-
return new URL(input).origin === new URL(origin).origin ? input : null;
|
|
11
|
-
} catch {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default } from './module.mjs'
|
|
2
2
|
|
|
3
|
-
export { type CheckoutBeforeContext, type CreatePaymentInput, type CreatePaymentResult, type ModuleOptions, type PaymentDto, type PaymentHookContext, type PaymentItem, type PaymentReference, type PaymentStatus, type PaymentsRuntimeConfig, type ResumeResponse } from './module.mjs'
|
|
3
|
+
export { type CheckoutBeforeContext, type CreatePaymentInput, type CreatePaymentResult, type InvoicePaymentInput, type ModuleOptions, type PaymentDto, type PaymentHookContext, type PaymentItem, type PaymentReference, type PaymentStatus, type PaymentsRuntimeConfig, type ResumeResponse } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/payments",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "One-time payments (Stripe Checkout, refunds and payment lifecycle hooks) for the frigear ecosystem, built on @frigear-nu/stripe.",
|
|
5
|
+
"homepage": "https://docs.frigear.dev/modules/payments",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "git+https://github.com/frigear-nu/frigear-nu.git",
|
|
@@ -29,9 +30,12 @@
|
|
|
29
30
|
"playground"
|
|
30
31
|
],
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@frigear-nu/
|
|
33
|
-
"@frigear-nu/
|
|
34
|
-
"@frigear-nu/
|
|
33
|
+
"@frigear-nu/auth": "0.0.3",
|
|
34
|
+
"@frigear-nu/mcp": "0.0.3",
|
|
35
|
+
"@frigear-nu/module-kit": "0.1.7",
|
|
36
|
+
"@frigear-nu/stripe": "0.0.8",
|
|
37
|
+
"@frigear-nu/ui": "0.0.7",
|
|
38
|
+
"@frigear-nu/users": "1.0.9",
|
|
35
39
|
"@libsql/client": "^0.18.0",
|
|
36
40
|
"@nuxt/kit": "^4.6.0",
|
|
37
41
|
"drizzle-kit": "^0.31.11",
|
|
@@ -42,15 +46,20 @@
|
|
|
42
46
|
"zod": "^4.6.5"
|
|
43
47
|
},
|
|
44
48
|
"peerDependencies": {
|
|
49
|
+
"@frigear-nu/widget": "0.0.2",
|
|
45
50
|
"@nuxt/ui": "^4.10.0",
|
|
46
51
|
"@nuxthub/core": "^0.10.8"
|
|
47
52
|
},
|
|
48
53
|
"peerDependenciesMeta": {
|
|
54
|
+
"@frigear-nu/widget": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
49
57
|
"@nuxt/ui": {
|
|
50
58
|
"optional": true
|
|
51
59
|
}
|
|
52
60
|
},
|
|
53
61
|
"devDependencies": {
|
|
62
|
+
"@frigear-nu/widget": "0.0.2",
|
|
54
63
|
"@nuxt/devtools": "^3.4.2",
|
|
55
64
|
"@nuxt/eslint-config": "^1.17.0",
|
|
56
65
|
"@nuxt/module-builder": "^1.0.3",
|