@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 +129 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +7 -0
- package/dist/module.d.ts +7 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +16 -0
- package/dist/runtime/server/checkoutHandler.d.ts +10 -0
- package/dist/runtime/server/checkoutHandler.js +78 -0
- package/dist/runtime/server/customerPortalHandler.d.ts +8 -0
- package/dist/runtime/server/customerPortalHandler.js +40 -0
- package/dist/runtime/server/index.d.ts +3 -0
- package/dist/runtime/server/index.js +3 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/webhookHandler.d.ts +5 -0
- package/dist/runtime/server/webhookHandler.js +54 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +60 -0
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) =>
|
package/dist/module.cjs
ADDED
package/dist/module.d.ts
ADDED
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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
|
+
};
|
package/dist/types.d.mts
ADDED
package/dist/types.d.ts
ADDED
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
|
+
}
|