Skip to content

Commit b33c27a

Browse files
authored
stripe plugin (#105)
* add stripe plugin * fixes
1 parent 8988183 commit b33c27a

File tree

9 files changed

+813
-1
lines changed

9 files changed

+813
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
8686
- **Linear**: Create Ticket, Find Issues
8787
- **Resend**: Send Email
8888
- **Slack**: Send Slack Message
89+
- **Stripe**: Create Customer, Get Customer, Create Invoice
8990
- **Superagent**: Guard, Redact
9091
- **v0**: Create Chat, Send Message
9192
<!-- PLUGINS:END -->

plugins/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* 1. Delete the plugin directory
1414
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
1515
*
16-
* Discovered plugins: ai-gateway, blob, firecrawl, github, linear, resend, slack, superagent, v0
16+
* Discovered plugins: ai-gateway, blob, firecrawl, github, linear, resend, slack, stripe, superagent, v0
1717
*/
1818

1919
import "./ai-gateway";
@@ -23,6 +23,7 @@ import "./github";
2323
import "./linear";
2424
import "./resend";
2525
import "./slack";
26+
import "./stripe";
2627
import "./superagent";
2728
import "./v0";
2829

plugins/stripe/credentials.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type StripeCredentials = {
2+
STRIPE_SECRET_KEY?: string;
3+
};
4+

plugins/stripe/icon.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function StripeIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
aria-label="Stripe logo"
5+
className={className}
6+
fill="currentColor"
7+
viewBox="0 0 24 24"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<title>Stripe</title>
11+
<path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.591-7.305z" />
12+
</svg>
13+
);
14+
}
15+

plugins/stripe/index.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import type { IntegrationPlugin } from "../registry";
2+
import { registerIntegration } from "../registry";
3+
import { StripeIcon } from "./icon";
4+
5+
const stripePlugin: IntegrationPlugin = {
6+
type: "stripe",
7+
label: "Stripe",
8+
description: "Payment processing and billing",
9+
10+
icon: StripeIcon,
11+
12+
formFields: [
13+
{
14+
id: "apiKey",
15+
label: "Secret Key",
16+
type: "password",
17+
placeholder: "sk_live_... or sk_test_...",
18+
configKey: "apiKey",
19+
envVar: "STRIPE_SECRET_KEY",
20+
helpText: "Get your secret key from ",
21+
helpLink: {
22+
text: "dashboard.stripe.com/apikeys",
23+
url: "https://dashboard.stripe.com/apikeys",
24+
},
25+
},
26+
],
27+
28+
testConfig: {
29+
getTestFunction: async () => {
30+
const { testStripe } = await import("./test");
31+
return testStripe;
32+
},
33+
},
34+
35+
actions: [
36+
{
37+
slug: "create-customer",
38+
label: "Create Customer",
39+
description: "Create a new customer in Stripe",
40+
category: "Stripe",
41+
stepFunction: "createCustomerStep",
42+
stepImportPath: "create-customer",
43+
outputFields: [
44+
{ field: "id", description: "Customer ID" },
45+
{ field: "email", description: "Customer email" },
46+
],
47+
configFields: [
48+
{
49+
key: "email",
50+
label: "Email",
51+
type: "template-input",
52+
placeholder: "customer@example.com or {{NodeName.email}}",
53+
example: "customer@example.com",
54+
required: true,
55+
},
56+
{
57+
key: "name",
58+
label: "Name",
59+
type: "template-input",
60+
placeholder: "John Doe or {{NodeName.name}}",
61+
example: "John Doe",
62+
},
63+
{
64+
key: "phone",
65+
label: "Phone",
66+
type: "template-input",
67+
placeholder: "+1234567890",
68+
example: "+1234567890",
69+
},
70+
{
71+
type: "group",
72+
label: "Additional Details",
73+
fields: [
74+
{
75+
key: "description",
76+
label: "Description",
77+
type: "template-input",
78+
placeholder: "Internal notes about this customer",
79+
example: "VIP customer",
80+
},
81+
{
82+
key: "metadata",
83+
label: "Metadata (JSON)",
84+
type: "template-textarea",
85+
placeholder: '{"key": "value"}',
86+
example: '{"plan": "enterprise", "source": "website"}',
87+
rows: 3,
88+
},
89+
],
90+
},
91+
],
92+
},
93+
{
94+
slug: "get-customer",
95+
label: "Get Customer",
96+
description: "Retrieve a customer by ID or email",
97+
category: "Stripe",
98+
stepFunction: "getCustomerStep",
99+
stepImportPath: "get-customer",
100+
outputFields: [
101+
{ field: "id", description: "Customer ID" },
102+
{ field: "email", description: "Customer email" },
103+
{ field: "name", description: "Customer name" },
104+
{ field: "created", description: "Creation timestamp" },
105+
],
106+
configFields: [
107+
{
108+
key: "customerId",
109+
label: "Customer ID",
110+
type: "template-input",
111+
placeholder: "cus_... or {{NodeName.customerId}}",
112+
example: "cus_ABC123",
113+
},
114+
{
115+
key: "email",
116+
label: "Email (alternative lookup)",
117+
type: "template-input",
118+
placeholder: "customer@example.com",
119+
example: "customer@example.com",
120+
},
121+
],
122+
},
123+
{
124+
slug: "create-invoice",
125+
label: "Create Invoice",
126+
description: "Create and optionally send an invoice",
127+
category: "Stripe",
128+
stepFunction: "createInvoiceStep",
129+
stepImportPath: "create-invoice",
130+
outputFields: [
131+
{ field: "id", description: "Invoice ID" },
132+
{ field: "number", description: "Invoice number" },
133+
{ field: "hostedInvoiceUrl", description: "Hosted invoice URL" },
134+
{ field: "status", description: "Invoice status" },
135+
],
136+
configFields: [
137+
{
138+
key: "customerId",
139+
label: "Customer ID",
140+
type: "template-input",
141+
placeholder: "cus_... or {{NodeName.customerId}}",
142+
example: "cus_ABC123",
143+
required: true,
144+
},
145+
{
146+
key: "description",
147+
label: "Description",
148+
type: "template-input",
149+
placeholder: "Invoice description",
150+
example: "Professional services - January 2024",
151+
},
152+
{
153+
key: "lineItems",
154+
label: "Line Items (JSON array)",
155+
type: "template-textarea",
156+
placeholder: '[{"description": "Item", "amount": 1000, "quantity": 1}]',
157+
example:
158+
'[{"description": "Consulting", "amount": 15000, "quantity": 2}]',
159+
rows: 4,
160+
required: true,
161+
},
162+
{
163+
type: "group",
164+
label: "Invoice Options",
165+
fields: [
166+
{
167+
key: "daysUntilDue",
168+
label: "Days Until Due",
169+
type: "number",
170+
defaultValue: "30",
171+
min: 1,
172+
},
173+
{
174+
key: "autoAdvance",
175+
label: "Auto-finalize",
176+
type: "select",
177+
options: [
178+
{ value: "true", label: "Yes" },
179+
{ value: "false", label: "No (draft)" },
180+
],
181+
defaultValue: "true",
182+
},
183+
{
184+
key: "collectionMethod",
185+
label: "Collection Method",
186+
type: "select",
187+
options: [
188+
{ value: "send_invoice", label: "Send Invoice" },
189+
{ value: "charge_automatically", label: "Charge Automatically" },
190+
],
191+
defaultValue: "send_invoice",
192+
},
193+
{
194+
key: "metadata",
195+
label: "Metadata (JSON)",
196+
type: "template-textarea",
197+
placeholder: '{"key": "value"}',
198+
example: '{"project": "website-redesign"}',
199+
rows: 3,
200+
},
201+
],
202+
},
203+
],
204+
},
205+
],
206+
};
207+
208+
registerIntegration(stripePlugin);
209+
210+
export default stripePlugin;
211+
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import "server-only";
2+
3+
import { fetchCredentials } from "@/lib/credential-fetcher";
4+
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
5+
import type { StripeCredentials } from "../credentials";
6+
7+
const STRIPE_API_URL = "https://api.stripe.com/v1";
8+
9+
type StripeCustomerResponse = {
10+
id: string;
11+
email: string;
12+
name: string | null;
13+
phone: string | null;
14+
created: number;
15+
};
16+
17+
type StripeErrorResponse = {
18+
error: {
19+
type: string;
20+
message: string;
21+
code?: string;
22+
};
23+
};
24+
25+
type CreateCustomerResult =
26+
| { success: true; id: string; email: string }
27+
| { success: false; error: string };
28+
29+
export type CreateCustomerCoreInput = {
30+
email: string;
31+
name?: string;
32+
phone?: string;
33+
description?: string;
34+
metadata?: string;
35+
};
36+
37+
export type CreateCustomerInput = StepInput &
38+
CreateCustomerCoreInput & {
39+
integrationId?: string;
40+
};
41+
42+
async function stepHandler(
43+
input: CreateCustomerCoreInput,
44+
credentials: StripeCredentials
45+
): Promise<CreateCustomerResult> {
46+
const apiKey = credentials.STRIPE_SECRET_KEY;
47+
48+
if (!apiKey) {
49+
return {
50+
success: false,
51+
error:
52+
"STRIPE_SECRET_KEY is not configured. Please add it in Project Integrations.",
53+
};
54+
}
55+
56+
try {
57+
const params = new URLSearchParams();
58+
params.append("email", input.email);
59+
60+
if (input.name) {
61+
params.append("name", input.name);
62+
}
63+
if (input.phone) {
64+
params.append("phone", input.phone);
65+
}
66+
if (input.description) {
67+
params.append("description", input.description);
68+
}
69+
if (input.metadata) {
70+
try {
71+
const metadataObj = JSON.parse(input.metadata) as Record<
72+
string,
73+
string
74+
>;
75+
for (const [key, value] of Object.entries(metadataObj)) {
76+
params.append(`metadata[${key}]`, String(value));
77+
}
78+
} catch {
79+
return {
80+
success: false,
81+
error: "Invalid metadata JSON format",
82+
};
83+
}
84+
}
85+
86+
const response = await fetch(`${STRIPE_API_URL}/customers`, {
87+
method: "POST",
88+
headers: {
89+
Authorization: `Bearer ${apiKey}`,
90+
"Content-Type": "application/x-www-form-urlencoded",
91+
},
92+
body: params.toString(),
93+
});
94+
95+
if (!response.ok) {
96+
const errorData = (await response.json()) as StripeErrorResponse;
97+
return {
98+
success: false,
99+
error:
100+
errorData.error?.message ||
101+
`HTTP ${response.status}: Failed to create customer`,
102+
};
103+
}
104+
105+
const data = (await response.json()) as StripeCustomerResponse;
106+
return { success: true, id: data.id, email: data.email };
107+
} catch (error) {
108+
const message = error instanceof Error ? error.message : String(error);
109+
return {
110+
success: false,
111+
error: `Failed to create customer: ${message}`,
112+
};
113+
}
114+
}
115+
116+
export async function createCustomerStep(
117+
input: CreateCustomerInput
118+
): Promise<CreateCustomerResult> {
119+
"use step";
120+
121+
const credentials = input.integrationId
122+
? await fetchCredentials(input.integrationId)
123+
: {};
124+
125+
return withStepLogging(input, () => stepHandler(input, credentials));
126+
}
127+
128+
export const _integrationType = "stripe";
129+

0 commit comments

Comments
 (0)