spaps-mcp 0.1.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.
@@ -0,0 +1,529 @@
1
+ # SPAPS Integration Step 7/12: Stripe Payment Integration
2
+
3
+ ## Prerequisites Check
4
+
5
+ Before starting, confirm you have completed:
6
+ - ✅ Step 1-6: Full authentication system working
7
+ - ✅ User can login and tokens are stored
8
+ - ✅ SDK initialized properly
9
+
10
+ ## Required TodoWrite List
11
+
12
+ Create a TodoWrite with EXACTLY these items:
13
+
14
+ ```javascript
15
+ TodoWrite({
16
+ todos: [
17
+ { content: "Create payments component directory", status: "pending", activeForm: "Creating payments directory" },
18
+ { content: "Implement listProducts with active filter", status: "pending", activeForm: "Implementing product listing" },
19
+ { content: "Use createPaymentCheckout for one-time payments", status: "pending", activeForm: "Implementing payment checkout" },
20
+ { content: "Use createSubscriptionCheckout for subscriptions", status: "pending", activeForm: "Implementing subscription checkout" },
21
+ { content: "Add createCustomerPortalSession", status: "pending", activeForm: "Adding customer portal" },
22
+ { content: "Handle checkout URL redirects", status: "pending", activeForm: "Handling redirects" },
23
+ { content: "Test with amount 999 ($9.99)", status: "pending", activeForm: "Testing payments" },
24
+ { content: "Request step 8 of SPAPS integration wizard", status: "pending", activeForm: "Requesting next step" }
25
+ ]
26
+ })
27
+ ```
28
+
29
+ ## Critical Method Names
30
+
31
+ ⚠️ **EXTREMELY IMPORTANT - CORRECT METHOD NAMES**:
32
+
33
+ ```typescript
34
+ // ✅ CORRECT Methods that exist:
35
+ sdk.payments.createPaymentCheckout() // One-time payment
36
+ sdk.payments.createSubscriptionCheckout() // Subscription
37
+ sdk.payments.listProducts() // Get products
38
+ sdk.payments.createCustomerPortalSession() // Customer portal
39
+
40
+ // ❌ WRONG - These variations DON'T exist:
41
+ sdk.payments.createCheckout() // NO! Too generic
42
+ sdk.payments.createCheckoutSession() // NO! Different name
43
+ sdk.payments.checkout() // NO! Doesn't exist
44
+ ```
45
+
46
+ ## Implementation Guide
47
+
48
+ ### 1. Payments Component
49
+
50
+ ```typescript
51
+ // components/payments/stripe-payments.tsx
52
+ 'use client'
53
+
54
+ import { useState, useEffect } from 'react'
55
+ import { Button } from "@/components/ui/button"
56
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
57
+ import { Badge } from "@/components/ui/badge"
58
+ import { Separator } from "@/components/ui/separator"
59
+ import { toast } from "sonner"
60
+ import { sdk } from '@/lib/spaps'
61
+ import { SweetPotatoAPIError } from 'spaps-sdk'
62
+ import { CreditCard, ShoppingCart, Settings, Loader2 } from 'lucide-react'
63
+
64
+ interface Product {
65
+ id: string
66
+ name: string
67
+ description?: string
68
+ prices: Array<{
69
+ id: string
70
+ amount: number
71
+ currency: string
72
+ interval?: string // 'month', 'year', etc.
73
+ intervalCount?: number
74
+ }>
75
+ }
76
+
77
+ export function StripePayments() {
78
+ const [isLoading, setIsLoading] = useState(false)
79
+ const [products, setProducts] = useState<Product[]>([])
80
+ const [loadingProducts, setLoadingProducts] = useState(true)
81
+
82
+ useEffect(() => {
83
+ loadProducts()
84
+ }, [])
85
+
86
+ const loadProducts = async () => {
87
+ try {
88
+ setLoadingProducts(true)
89
+
90
+ // ✅ CORRECT: List products from Stripe
91
+ const productList = await sdk.payments.listProducts({
92
+ active: true,
93
+ limit: 10
94
+ })
95
+
96
+ setProducts(productList.products || [])
97
+ } catch (error) {
98
+ if (error instanceof SweetPotatoAPIError) {
99
+ toast.error(`Failed to load products: ${error.message}`)
100
+ } else {
101
+ toast.error('Failed to load products')
102
+ }
103
+ console.error('Load products error:', error)
104
+ } finally {
105
+ setLoadingProducts(false)
106
+ }
107
+ }
108
+
109
+ const handleOneTimePayment = async () => {
110
+ setIsLoading(true)
111
+ try {
112
+ // ✅ CORRECT: Use createPaymentCheckout for one-time payments
113
+ const checkout = await sdk.payments.createPaymentCheckout({
114
+ product_name: 'Test Product',
115
+ amount: 999, // $9.99 in cents
116
+ currency: 'usd',
117
+ quantity: 1,
118
+ success_url: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
119
+ cancel_url: `${window.location.origin}/cancel`,
120
+ metadata: {
121
+ userId: 'user_123',
122
+ source: 'demo_app'
123
+ }
124
+ })
125
+
126
+ if (checkout.url) {
127
+ // Redirect to Stripe Checkout
128
+ window.location.href = checkout.url
129
+ } else {
130
+ toast.error('Failed to create checkout session')
131
+ }
132
+
133
+ } catch (error) {
134
+ handlePaymentError(error)
135
+ } finally {
136
+ setIsLoading(false)
137
+ }
138
+ }
139
+
140
+ const handleSubscription = async (priceId: string) => {
141
+ setIsLoading(true)
142
+ try {
143
+ // ✅ CORRECT: Use createSubscriptionCheckout for subscriptions
144
+ const checkout = await sdk.payments.createSubscriptionCheckout({
145
+ price_id: priceId,
146
+ success_url: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
147
+ cancel_url: `${window.location.origin}/cancel`,
148
+ trial_period_days: 7, // Optional trial
149
+ metadata: {
150
+ userId: 'user_123'
151
+ }
152
+ })
153
+
154
+ if (checkout.url) {
155
+ window.location.href = checkout.url
156
+ } else {
157
+ toast.error('Failed to create subscription checkout')
158
+ }
159
+
160
+ } catch (error) {
161
+ handlePaymentError(error)
162
+ } finally {
163
+ setIsLoading(false)
164
+ }
165
+ }
166
+
167
+ const handleCustomerPortal = async () => {
168
+ setIsLoading(true)
169
+ try {
170
+ // ✅ CORRECT: Create customer portal session
171
+ const portal = await sdk.payments.createCustomerPortalSession({
172
+ return_url: window.location.href
173
+ })
174
+
175
+ if (portal.url) {
176
+ window.location.href = portal.url
177
+ } else {
178
+ toast.error('Failed to access customer portal')
179
+ }
180
+
181
+ } catch (error) {
182
+ handlePaymentError(error)
183
+ } finally {
184
+ setIsLoading(false)
185
+ }
186
+ }
187
+
188
+ const handlePaymentError = (error: any) => {
189
+ if (error instanceof SweetPotatoAPIError) {
190
+ switch (error.code) {
191
+ case 'PAYMENT_REQUIRED':
192
+ toast.error('Payment required to continue')
193
+ break
194
+ case 'INSUFFICIENT_FUNDS':
195
+ toast.error('Payment failed: Insufficient funds')
196
+ break
197
+ case 'CARD_DECLINED':
198
+ toast.error('Card was declined')
199
+ break
200
+ default:
201
+ toast.error(error.message || 'Payment failed')
202
+ }
203
+ } else {
204
+ toast.error('An unexpected error occurred')
205
+ }
206
+ console.error('Payment error:', error)
207
+ }
208
+
209
+ const formatPrice = (amount: number, currency: string) => {
210
+ return new Intl.NumberFormat('en-US', {
211
+ style: 'currency',
212
+ currency: currency.toUpperCase(),
213
+ }).format(amount / 100)
214
+ }
215
+
216
+ return (
217
+ <div className="space-y-6">
218
+ {/* One-Time Payment */}
219
+ <Card>
220
+ <CardHeader>
221
+ <CardTitle className="flex items-center gap-2">
222
+ <CreditCard className="h-5 w-5" />
223
+ One-Time Payment
224
+ </CardTitle>
225
+ <CardDescription>
226
+ Test a single payment with Stripe Checkout
227
+ </CardDescription>
228
+ </CardHeader>
229
+ <CardContent className="space-y-4">
230
+ <div className="p-4 bg-muted rounded-lg">
231
+ <p className="text-sm font-medium">Test Product</p>
232
+ <p className="text-2xl font-bold">$9.99</p>
233
+ <p className="text-xs text-muted-foreground">One-time payment</p>
234
+ </div>
235
+
236
+ <Button
237
+ onClick={handleOneTimePayment}
238
+ className="w-full"
239
+ disabled={isLoading}
240
+ >
241
+ {isLoading ? (
242
+ <>
243
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
244
+ Processing...
245
+ </>
246
+ ) : (
247
+ <>
248
+ <ShoppingCart className="mr-2 h-4 w-4" />
249
+ Pay $9.99 Now
250
+ </>
251
+ )}
252
+ </Button>
253
+
254
+ <p className="text-xs text-muted-foreground text-center">
255
+ Test card: 4242 4242 4242 4242
256
+ </p>
257
+ </CardContent>
258
+ </Card>
259
+
260
+ {/* Products List */}
261
+ <Card>
262
+ <CardHeader>
263
+ <CardTitle>Available Products</CardTitle>
264
+ <CardDescription>
265
+ Products and subscriptions from your Stripe account
266
+ </CardDescription>
267
+ </CardHeader>
268
+ <CardContent>
269
+ {loadingProducts ? (
270
+ <div className="flex justify-center py-8">
271
+ <Loader2 className="h-6 w-6 animate-spin" />
272
+ </div>
273
+ ) : products.length === 0 ? (
274
+ <div className="text-center py-8 text-muted-foreground">
275
+ No products available
276
+ </div>
277
+ ) : (
278
+ <div className="space-y-4">
279
+ {products.map((product) => (
280
+ <div key={product.id} className="border rounded-lg p-4">
281
+ <h4 className="font-medium">{product.name}</h4>
282
+ {product.description && (
283
+ <p className="text-sm text-muted-foreground mb-2">
284
+ {product.description}
285
+ </p>
286
+ )}
287
+ <div className="space-y-2">
288
+ {product.prices.map((price) => (
289
+ <div key={price.id} className="flex items-center justify-between">
290
+ <div>
291
+ <span className="text-sm font-medium">
292
+ {formatPrice(price.amount, price.currency)}
293
+ </span>
294
+ {price.interval && (
295
+ <span className="text-xs text-muted-foreground ml-1">
296
+ / {price.interval}
297
+ </span>
298
+ )}
299
+ </div>
300
+ <Button
301
+ size="sm"
302
+ onClick={() =>
303
+ price.interval
304
+ ? handleSubscription(price.id)
305
+ : handleOneTimePayment()
306
+ }
307
+ disabled={isLoading}
308
+ >
309
+ {price.interval ? 'Subscribe' : 'Buy Now'}
310
+ </Button>
311
+ </div>
312
+ ))}
313
+ </div>
314
+ </div>
315
+ ))}
316
+ </div>
317
+ )}
318
+ </CardContent>
319
+ </Card>
320
+
321
+ {/* Customer Portal */}
322
+ <Card>
323
+ <CardHeader>
324
+ <CardTitle className="flex items-center gap-2">
325
+ <Settings className="h-5 w-5" />
326
+ Customer Portal
327
+ </CardTitle>
328
+ <CardDescription>
329
+ Manage your subscriptions and billing
330
+ </CardDescription>
331
+ </CardHeader>
332
+ <CardContent>
333
+ <Button
334
+ onClick={handleCustomerPortal}
335
+ className="w-full"
336
+ disabled={isLoading}
337
+ variant="outline"
338
+ >
339
+ Open Customer Portal
340
+ </Button>
341
+ <p className="text-xs text-muted-foreground text-center mt-2">
342
+ View invoices, update payment methods, cancel subscriptions
343
+ </p>
344
+ </CardContent>
345
+ </Card>
346
+ </div>
347
+ )
348
+ }
349
+ ```
350
+
351
+ ### 2. Success/Cancel Pages
352
+
353
+ Create pages to handle checkout redirects:
354
+
355
+ ```typescript
356
+ // app/success/page.tsx
357
+ 'use client'
358
+
359
+ import { useEffect } from 'react'
360
+ import { useSearchParams } from 'next/navigation'
361
+ import { Card } from "@/components/ui/card"
362
+ import { Button } from "@/components/ui/button"
363
+ import Link from 'next/link'
364
+ import { CheckCircle } from 'lucide-react'
365
+
366
+ export default function PaymentSuccess() {
367
+ const searchParams = useSearchParams()
368
+ const sessionId = searchParams.get('session_id')
369
+
370
+ useEffect(() => {
371
+ // You could verify the session here if needed
372
+ if (sessionId) {
373
+ console.log('Payment successful! Session:', sessionId)
374
+ }
375
+ }, [sessionId])
376
+
377
+ return (
378
+ <div className="min-h-screen flex items-center justify-center p-4">
379
+ <Card className="w-[400px] p-8">
380
+ <div className="text-center space-y-4">
381
+ <CheckCircle className="h-16 w-16 text-green-600 mx-auto" />
382
+ <h1 className="text-2xl font-bold">Payment Successful!</h1>
383
+ <p className="text-muted-foreground">
384
+ Your payment has been processed successfully.
385
+ </p>
386
+ {sessionId && (
387
+ <p className="text-xs text-muted-foreground font-mono">
388
+ Session: {sessionId.substring(0, 20)}...
389
+ </p>
390
+ )}
391
+ <Button asChild className="w-full">
392
+ <Link href="/dashboard">
393
+ Back to Dashboard
394
+ </Link>
395
+ </Button>
396
+ </div>
397
+ </Card>
398
+ </div>
399
+ )
400
+ }
401
+ ```
402
+
403
+ ## Method Details
404
+
405
+ ### createPaymentCheckout
406
+
407
+ For one-time payments:
408
+ ```typescript
409
+ sdk.payments.createPaymentCheckout({
410
+ product_name?: string, // Product name
411
+ amount?: number, // Amount in cents
412
+ currency?: string, // Currency code (usd, eur, etc.)
413
+ quantity?: number, // Quantity
414
+ price_id?: string, // Or use existing price ID
415
+ success_url: string, // Where to redirect on success
416
+ cancel_url: string, // Where to redirect on cancel
417
+ metadata?: Record<string, string> // Custom metadata
418
+ })
419
+ ```
420
+
421
+ ### createSubscriptionCheckout
422
+
423
+ For recurring payments:
424
+ ```typescript
425
+ sdk.payments.createSubscriptionCheckout({
426
+ price_id: string, // Stripe price ID (required)
427
+ success_url: string, // Where to redirect on success
428
+ cancel_url: string, // Where to redirect on cancel
429
+ trial_period_days?: number, // Optional trial
430
+ metadata?: Record<string, string> // Custom metadata
431
+ })
432
+ ```
433
+
434
+ ## Validation Checklist
435
+
436
+ ✅ **Method Names**:
437
+ - Uses `createPaymentCheckout` for one-time
438
+ - Uses `createSubscriptionCheckout` for recurring
439
+ - NOT using generic `createCheckout`
440
+
441
+ ✅ **Implementation**:
442
+ - Products load from Stripe
443
+ - Checkout redirects work
444
+ - Success/cancel pages exist
445
+ - Customer portal accessible
446
+
447
+ ✅ **Testing**:
448
+ - Test payment with $9.99 (999 cents)
449
+ - Use test card: 4242 4242 4242 4242
450
+ - Any future expiry, any CVC
451
+ - Should redirect to Stripe
452
+
453
+ ## Common Mistakes to Avoid
454
+
455
+ ❌ **Wrong method names**:
456
+ ```typescript
457
+ // WRONG - These don't exist
458
+ sdk.payments.createCheckout()
459
+ sdk.payments.checkout()
460
+ sdk.payments.createSession()
461
+ ```
462
+
463
+ ❌ **Direct Stripe integration**:
464
+ ```typescript
465
+ // WRONG - Use SDK instead
466
+ import Stripe from 'stripe'
467
+ const stripe = new Stripe(...)
468
+ ```
469
+
470
+ ❌ **Wrong amount format**:
471
+ ```typescript
472
+ // WRONG - Amount should be in cents
473
+ amount: 9.99 // Should be 999
474
+ ```
475
+
476
+ ❌ **Missing redirects**:
477
+ ```typescript
478
+ // WRONG - Must include both URLs
479
+ createPaymentCheckout({
480
+ amount: 999
481
+ // Missing success_url and cancel_url!
482
+ })
483
+ ```
484
+
485
+ ## Testing Stripe Integration
486
+
487
+ ### Test Cards
488
+
489
+ - **Success**: 4242 4242 4242 4242
490
+ - **Decline**: 4000 0000 0000 0002
491
+ - **Requires Auth**: 4000 0025 0000 3155
492
+
493
+ ### Local Testing
494
+
495
+ In local mode:
496
+ 1. Uses Stripe test mode automatically
497
+ 2. No real charges occur
498
+ 3. Can use test cards
499
+ 4. Webhooks handled by SPAPS
500
+
501
+ ### Verification
502
+
503
+ After payment:
504
+ 1. Check Stripe Dashboard (test mode)
505
+ 2. Verify session completed
506
+ 3. Check user's payment status
507
+ 4. Test customer portal access
508
+
509
+ ## Next Step
510
+
511
+ When ALL todos are ✅ complete and payments work:
512
+
513
+ ```javascript
514
+ mcp__product-manager__get_agent_instructions({
515
+ category: "spaps-integration",
516
+ project: "spaps-demo",
517
+ step: 8
518
+ })
519
+ ```
520
+
521
+ ## Summary
522
+
523
+ Stripe integration via SDK provides:
524
+ - **Simple checkout** - Just redirect to Stripe
525
+ - **Secure payments** - PCI compliance handled
526
+ - **Subscriptions** - Recurring payments supported
527
+ - **Customer portal** - Self-service management
528
+
529
+ Remember: Use the EXACT method names - this is where most errors occur!