@spaire/nuxt 2.0.0

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 ADDED
@@ -0,0 +1,129 @@
1
+ # @spaire/nuxt
2
+
3
+ Payments and Checkouts made dead simple with Nuxt.
4
+
5
+ ## Installation
6
+
7
+ ### Install the package
8
+
9
+ Choose your preferred package manager to install the module:
10
+
11
+ `pnpm add @spaire/nuxt`
12
+
13
+ ### Register the module
14
+
15
+ Add the module to your `nuxt.config.ts`:
16
+
17
+ ```typescript
18
+ export default defineNuxtConfig({
19
+ modules: ["@spaire/nuxt"],
20
+ });
21
+ ```
22
+
23
+ ## Checkout
24
+
25
+ Create a Checkout handler which takes care of redirections.
26
+
27
+ ```typescript
28
+ // server/routes/api/checkout.post.ts
29
+ export default defineEventHandler((event) => {
30
+ const {
31
+ private: { spaireAccessToken, spaireCheckoutSuccessUrl, spaireServer },
32
+ } = useRuntimeConfig();
33
+
34
+ const checkoutHandler = Checkout({
35
+ accessToken: spaireAccessToken,
36
+ successUrl: spaireCheckoutSuccessUrl,
37
+ returnUrl: "https://myapp.com", // Optional Return URL, which renders a Back-button in the Checkout
38
+ server: spaireServer as "sandbox" | "production",
39
+ theme: "dark" // Enforces the theme - System-preferred theme will be set if left omitted
40
+ });
41
+
42
+ return checkoutHandler(event);
43
+ });
44
+ ```
45
+
46
+ ### Query Params
47
+
48
+ Pass query params to this route.
49
+
50
+ - products `?products=123`
51
+ - customerId (optional) `?products=123&customerId=xxx`
52
+ - customerExternalId (optional) `?products=123&customerExternalId=xxx`
53
+ - customerEmail (optional) `?products=123&customerEmail=janedoe@gmail.com`
54
+ - customerName (optional) `?products=123&customerName=Jane`
55
+ - metadata (optional) `URL-Encoded JSON string`
56
+
57
+ ## Customer Portal
58
+
59
+ Create a customer portal where your customer can view orders and subscriptions.
60
+
61
+ ```typescript
62
+ // server/routes/api/portal.get.ts
63
+ export default defineEventHandler((event) => {
64
+ const {
65
+ private: { spaireAccessToken, spaireCheckoutSuccessUrl, spaireServer },
66
+ } = useRuntimeConfig();
67
+
68
+ const customerPortalHandler = CustomerPortal({
69
+ accessToken: spaireAccessToken,
70
+ server: spaireServer as "sandbox" | "production",
71
+ getCustomerId: (event) => {
72
+ return Promise.resolve("9d89909b-216d-475e-8005-053dba7cff07");
73
+ },
74
+ returnUrl: "https://myapp.com", // Optional Return URL, which renders a Back-button in the Customer Portal
75
+ });
76
+
77
+ return customerPortalHandler(event);
78
+ });
79
+ ```
80
+
81
+ ## Webhooks
82
+
83
+ A simple utility which resolves incoming webhook payloads by signing the webhook secret properly.
84
+
85
+ ```typescript
86
+ // server/routes/webhook/spaire.post.ts
87
+ export default defineEventHandler((event) => {
88
+ const {
89
+ private: { spaireWebhookSecret },
90
+ } = useRuntimeConfig();
91
+
92
+ const webhooksHandler = Webhooks({
93
+ webhookSecret: spaireWebhookSecret,
94
+ onPayload: async (payload: any) => {
95
+ // Handle the payload
96
+ // No need to return an acknowledge response
97
+ },
98
+ });
99
+
100
+ return webhooksHandler(event);
101
+ });
102
+ ```
103
+
104
+ ### Payload Handlers
105
+
106
+ The Webhook handler also supports granular handlers for easy integration.
107
+
108
+ - onCheckoutCreated: (payload) =>
109
+ - onCheckoutUpdated: (payload) =>
110
+ - onOrderCreated: (payload) =>
111
+ - onOrderUpdated: (payload) =>
112
+ - onOrderPaid: (payload) =>
113
+ - onSubscriptionCreated: (payload) =>
114
+ - onSubscriptionUpdated: (payload) =>
115
+ - onSubscriptionActive: (payload) =>
116
+ - onSubscriptionCanceled: (payload) =>
117
+ - onSubscriptionRevoked: (payload) =>
118
+ - onProductCreated: (payload) =>
119
+ - onProductUpdated: (payload) =>
120
+ - onOrganizationUpdated: (payload) =>
121
+ - onBenefitCreated: (payload) =>
122
+ - onBenefitUpdated: (payload) =>
123
+ - onBenefitGrantCreated: (payload) =>
124
+ - onBenefitGrantUpdated: (payload) =>
125
+ - onBenefitGrantRevoked: (payload) =>
126
+ - onCustomerCreated: (payload) =>
127
+ - onCustomerUpdated: (payload) =>
128
+ - onCustomerDeleted: (payload) =>
129
+ - onCustomerStateChanged: (payload) =>
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,7 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ type ModuleOptions = Record<string, unknown>;
4
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+
6
+ export { _default as default };
7
+ export type { ModuleOptions };
@@ -0,0 +1,7 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ type ModuleOptions = Record<string, unknown>;
4
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+
6
+ export { _default as default };
7
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@spaire/nuxt",
3
+ "configKey": "spaire",
4
+ "version": "2.0.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "0.8.4",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -0,0 +1,16 @@
1
+ import { defineNuxtModule, createResolver, addServerImportsDir } from '@nuxt/kit';
2
+
3
+ const module = defineNuxtModule({
4
+ meta: {
5
+ name: "@spaire/nuxt",
6
+ configKey: "spaire"
7
+ },
8
+ // Default configuration options of the Nuxt module
9
+ defaults: {},
10
+ setup(_options, _nuxt) {
11
+ const resolver = createResolver(import.meta.url);
12
+ addServerImportsDir(resolver.resolve("./runtime/server"));
13
+ }
14
+ });
15
+
16
+ export { module as default };
@@ -0,0 +1,10 @@
1
+ import type { H3Event } from "h3";
2
+ export interface CheckoutConfig {
3
+ accessToken?: string;
4
+ successUrl?: string;
5
+ returnUrl?: string;
6
+ includeCheckoutId?: boolean;
7
+ server?: "sandbox" | "production";
8
+ theme?: "light" | "dark";
9
+ }
10
+ export declare const Checkout: ({ accessToken, successUrl, returnUrl, server, theme, includeCheckoutId, }: CheckoutConfig) => (event: H3Event) => Promise<void>;
@@ -0,0 +1,78 @@
1
+ import { Spaire } from "@spaire/sdk";
2
+ import { createError, getValidatedQuery, sendRedirect } from "h3";
3
+ import { z } from "zod";
4
+ const checkoutQuerySchema = z.object({
5
+ products: z.string().transform((value) => value.split(",")).pipe(z.string().array()),
6
+ customerId: z.string().nonempty().optional(),
7
+ customerExternalId: z.string().nonempty().optional(),
8
+ customerEmail: z.string().email().optional(),
9
+ customerName: z.string().nonempty().optional(),
10
+ customerBillingAddress: z.string().nonempty().optional(),
11
+ customerTaxId: z.string().nonempty().optional(),
12
+ customerIpAddress: z.string().nonempty().optional(),
13
+ customerMetadata: z.string().nonempty().optional(),
14
+ allowDiscountCodes: z.string().toLowerCase().transform((x) => x === "true").pipe(z.boolean()).optional(),
15
+ discountId: z.string().nonempty().optional(),
16
+ metadata: z.string().nonempty().optional()
17
+ });
18
+ export const Checkout = ({
19
+ accessToken,
20
+ successUrl,
21
+ returnUrl,
22
+ server,
23
+ theme,
24
+ includeCheckoutId = true
25
+ }) => {
26
+ return async (event) => {
27
+ const {
28
+ products,
29
+ customerId,
30
+ customerExternalId,
31
+ customerEmail,
32
+ customerName,
33
+ customerBillingAddress,
34
+ customerTaxId,
35
+ customerIpAddress,
36
+ customerMetadata,
37
+ allowDiscountCodes,
38
+ discountId,
39
+ metadata
40
+ } = await getValidatedQuery(event, checkoutQuerySchema.parse);
41
+ try {
42
+ const success = successUrl ? new URL(successUrl) : void 0;
43
+ if (success && includeCheckoutId) {
44
+ success.searchParams.set("checkoutId", "{CHECKOUT_ID}");
45
+ }
46
+ const retUrl = returnUrl ? new URL(returnUrl) : void 0;
47
+ const spaire = new Spaire({ accessToken, server });
48
+ const result = await spaire.checkouts.create({
49
+ products,
50
+ successUrl: success ? decodeURI(success.toString()) : void 0,
51
+ customerId,
52
+ externalCustomerId: customerExternalId,
53
+ customerEmail,
54
+ customerName,
55
+ customerBillingAddress: customerBillingAddress ? JSON.parse(customerBillingAddress) : void 0,
56
+ customerTaxId,
57
+ customerIpAddress,
58
+ customerMetadata: customerMetadata ? JSON.parse(customerMetadata) : void 0,
59
+ allowDiscountCodes,
60
+ discountId,
61
+ metadata: metadata ? JSON.parse(metadata) : void 0,
62
+ returnUrl: retUrl ? decodeURI(retUrl.toString()) : void 0
63
+ });
64
+ const redirectUrl = new URL(result.url);
65
+ if (theme) {
66
+ redirectUrl.searchParams.set("theme", theme);
67
+ }
68
+ return sendRedirect(event, redirectUrl.toString());
69
+ } catch (error) {
70
+ console.error("Failed to checkout:", error);
71
+ throw createError({
72
+ statusCode: 500,
73
+ statusMessage: error.message,
74
+ message: error.message ?? "Internal server error"
75
+ });
76
+ }
77
+ };
78
+ };
@@ -0,0 +1,8 @@
1
+ import type { H3Event } from "h3";
2
+ export interface CustomerPortalConfig {
3
+ accessToken: string;
4
+ server?: "sandbox" | "production";
5
+ getCustomerId: (event: H3Event) => Promise<string>;
6
+ returnUrl?: string;
7
+ }
8
+ export declare const CustomerPortal: ({ accessToken, server, getCustomerId, returnUrl, }: CustomerPortalConfig) => (event: H3Event) => Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { Spaire } from "@spaire/sdk";
2
+ import { createError, sendRedirect } from "h3";
3
+ export const CustomerPortal = ({
4
+ accessToken,
5
+ server,
6
+ getCustomerId,
7
+ returnUrl
8
+ }) => {
9
+ return async (event) => {
10
+ const retUrl = returnUrl ? new URL(returnUrl) : void 0;
11
+ const customerId = await getCustomerId(event);
12
+ if (!customerId) {
13
+ console.error(
14
+ "Failed to redirect to customer portal, customerId not defined"
15
+ );
16
+ throw createError({
17
+ statusCode: 400,
18
+ message: "customerId not defined"
19
+ });
20
+ }
21
+ try {
22
+ const spaire = new Spaire({
23
+ accessToken,
24
+ server
25
+ });
26
+ const result = await spaire.customerSessions.create({
27
+ customerId,
28
+ returnUrl: retUrl ? decodeURI(retUrl.toString()) : void 0
29
+ });
30
+ return sendRedirect(event, result.customerPortalUrl);
31
+ } catch (error) {
32
+ console.error("Failed to redirect to customer portal", error);
33
+ throw createError({
34
+ statusCode: 500,
35
+ statusMessage: error.message,
36
+ message: error.message ?? "Internal server error"
37
+ });
38
+ }
39
+ };
40
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./checkoutHandler.js";
2
+ export * from "./customerPortalHandler.js";
3
+ export * from "./webhookHandler.js";
@@ -0,0 +1,3 @@
1
+ export * from "./checkoutHandler.js";
2
+ export * from "./customerPortalHandler.js";
3
+ export * from "./webhookHandler.js";
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json"
3
+ }
@@ -0,0 +1,5 @@
1
+ import type { WebhooksConfig } from "@spaire/adapter-utils";
2
+ import type { H3Event } from "h3";
3
+ export declare const Webhooks: ({ webhookSecret, onPayload, entitlements, ...eventHandlers }: WebhooksConfig) => (event: H3Event) => Promise<{
4
+ received: boolean;
5
+ }>;
@@ -0,0 +1,54 @@
1
+ import { handleWebhookPayload } from "@spaire/adapter-utils";
2
+ import { WebhookVerificationError, validateEvent } from "@spaire/sdk/webhooks";
3
+ import { createError, getHeader, readRawBody, setResponseStatus } from "h3";
4
+ export const Webhooks = ({
5
+ webhookSecret,
6
+ onPayload,
7
+ entitlements,
8
+ ...eventHandlers
9
+ }) => {
10
+ return async (event) => {
11
+ const requestBody = await readRawBody(event);
12
+ const webhookHeaders = {
13
+ "webhook-id": getHeader(event, "webhook-id") ?? "",
14
+ "webhook-timestamp": getHeader(event, "webhook-timestamp") ?? "",
15
+ "webhook-signature": getHeader(event, "webhook-signature") ?? ""
16
+ };
17
+ let webhookPayload;
18
+ try {
19
+ webhookPayload = validateEvent(
20
+ requestBody || "",
21
+ webhookHeaders,
22
+ webhookSecret
23
+ );
24
+ } catch (error) {
25
+ if (error instanceof WebhookVerificationError) {
26
+ console.error("Failed to verify webhook event", error);
27
+ setResponseStatus(event, 403);
28
+ return { received: false };
29
+ }
30
+ console.error("Failed to validate webhook event", error);
31
+ throw createError({
32
+ statusCode: 500,
33
+ statusMessage: error.message,
34
+ message: error.message ?? "Internal server error"
35
+ });
36
+ }
37
+ try {
38
+ await handleWebhookPayload(webhookPayload, {
39
+ webhookSecret,
40
+ entitlements,
41
+ onPayload,
42
+ ...eventHandlers
43
+ });
44
+ return { received: true };
45
+ } catch (error) {
46
+ console.error("Webhook error", error);
47
+ throw createError({
48
+ statusCode: 500,
49
+ statusMessage: error.message,
50
+ message: error.message ?? "Internal server error"
51
+ });
52
+ }
53
+ };
54
+ };
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.js'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.js'
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module'
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@spaire/nuxt",
3
+ "version": "2.0.0",
4
+ "description": "Spaire integration for Nuxt",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/spaire/spaire.git"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types.d.ts",
14
+ "import": "./dist/module.mjs",
15
+ "require": "./dist/module.cjs"
16
+ }
17
+ },
18
+ "main": "./dist/module.cjs",
19
+ "types": "./dist/types.d.ts",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "nuxt-module-build build",
25
+ "prepare": "nuxt-module-build prepare",
26
+ "dev": "nuxi dev playground",
27
+ "dev:build": "nuxi build playground",
28
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
29
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
30
+ "lint": "eslint .",
31
+ "test": "vitest",
32
+ "test:watch": "vitest watch",
33
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
34
+ "check": "biome check --write ./src"
35
+ },
36
+ "dependencies": {
37
+ "@nuxt/kit": "^3.15.4",
38
+ "@spaire/adapter-utils": "^2.0.0",
39
+ "@spaire/sdk": "^0.45.1"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "1.9.4",
43
+ "@nuxt/devtools": "^2.6.5",
44
+ "@nuxt/eslint-config": "^1.11.0",
45
+ "@nuxt/module-builder": "^0.8.4",
46
+ "@nuxt/schema": "^3.15.4",
47
+ "@nuxt/test-utils": "^3.21.0",
48
+ "@types/node": "latest",
49
+ "changelogen": "^0.6.2",
50
+ "eslint": "^9.39.1",
51
+ "nuxt": "^3.15.4",
52
+ "typescript": "5.9.3",
53
+ "vitest": "^3.2.4",
54
+ "vue-tsc": "2.1.6",
55
+ "zod": "^3.24.2"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }