Token导航 LogoToken导航

Agent Skill · 前端设计

stripe-integration(Stripe 集成)

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

stripe-integration 是前端设计类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证为 MIT,第三方扫描有未通过项,按安装量在前端设计类的 10,216 个 Skill 里排第 7,947。

总安装 326周安装 14GitHub Stars 127下载 114核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
MIT

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:stripe-integration(Stripe 集成)
来源仓库:https://github.com/anton-abyzov/specweave
仓库路径:skills/stripe-integration
安装命令:
npx skills add https://github.com/anton-abyzov/specweave --skill stripe-integration
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/anton-abyzov/specweave --skill stripe-integration

这个 Skill 是做什么的

前端设计类 Skill 关注页面结构、视觉规范和组件写法,输出通常是可运行的界面代码。

提供 Stripe 支付全流程集成支持,包括订阅、Webhook、Connect 市场支付等场景。覆盖 PCI 合规、SCA 认证、退款争议与客户支付方式管理。可生成移动端与 Web 端的支付验证代码与安全最佳实践。需妥善保管 API 密钥,避免硬编码并启用 webhook 签名验证。

建议先在沙盒环境测试完整支付链路,再切换至生产模式。

来源仓库内路径:skills/stripe-integration

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/anton-abyzov/specweave --skill stripe-integrat…;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

stripe-integration 怎么安装?

在终端执行 npx skills add https://github.com/anton-abyzov/specweave --skill stripe-integration。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

stripe-integration 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

stripe-integration 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

stripe-integration 是免费的吗?

来源标注的许可证是 MIT。本站不代收任何费用,也不托管安装包;能否商用、要不要署名,以许可证原文为准。

stripe-integration 用的人多吗?

公开数据是累计安装 326 次,近一周安装 14 次,下载 114 次,GitHub 127 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

stripe-integration 安全吗?

第三方扫描有未通过项,风险较高。除非你确认过源码,否则不建议直接安装到常用环境。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 3 个目录同时收录,说明这个 Skill 流传比较广。
  • 许可证明确为 MIT,商用和分发边界好判断。

先想清楚的地方

  • 第三方扫描存在提醒项或未通过项,需要人工逐条确认。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理前端设计类任务、又不想每次都手写提示词的人。

不太适合

  • 需要厂商级技术支持和 SLA 的生产系统。

前端设计类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
frontend-design

前端设计 · Codex、Claude

8,823,360126,072只读MIT通过
vercel-composition-patterns

Vercel 组合模式 · Codex、Claude

3,858,81625,893需要联网MIT通过
vercel-react-native-skills

Vercel React native skills 前端 · Codex、Claude

2,468,66425,878只读MIT通过
adapt

adapt 搜索 · Codex、Claude

2,027,04023,125操作浏览器未明确通过
bolder

bolder 分析 · Codex、Claude

2,004,28823,130只读未明确通过
stripe-integration

Stripe 集成 · 本页

326127需要联网MIT未通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Stripe Integration

Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, Stripe Connect marketplace payments, and mobile/web payment verification.

When to Use This Skill

  • Implementing payment processing in web/mobile applications
  • Setting up subscription billing systems
  • Handling one-time payments and recurring charges
  • Processing refunds and disputes
  • Managing customer payment methods
  • Implementing SCA (Strong Customer Authentication) for European payments
  • Building marketplace payment flows with Stripe Connect
  • Implementing Direct Charge or Destination Charge patterns
  • Handling promo codes and 100% discount scenarios
  • Implementing dual confirmation (webhook + frontend verification)
  • Managing inventory/slots with payment atomicity

Core Concepts

1. Payment Flows

Checkout Session (Hosted)

  • Stripe-hosted payment page
  • Minimal PCI compliance burden
  • Fastest implementation
  • Supports one-time and recurring payments

Payment Intents (Custom UI)

  • Full control over payment UI
  • Requires Stripe.js for PCI compliance
  • More complex implementation
  • Better customization options

Setup Intents (Save Payment Methods)

  • Collect payment method without charging
  • Used for subscriptions and future payments
  • Requires customer confirmation

2. Webhooks

Critical Events:

  • payment_intent.succeeded: Payment completed
  • payment_intent.payment_failed: Payment failed
  • checkout.session.completed: Checkout session finished (CRITICAL for Connect!)
  • checkout.session.expired: Checkout session timed out
  • customer.subscription.updated: Subscription changed
  • customer.subscription.deleted: Subscription canceled
  • charge.refunded: Refund processed
  • invoice.payment_succeeded: Subscription payment successful
  • account.updated: Connect account status changed
  • payout.paid / payout.failed: Payout status for Connect accounts

3. Subscriptions

Components:

  • Product: What you're selling
  • Price: How much and how often
  • Subscription: Customer's recurring payment
  • Invoice: Generated for each billing cycle

4. Customer Management

  • Create and manage customer records
  • Store multiple payment methods
  • Track customer metadata
  • Manage billing details

5. Stripe Connect (Marketplace/Platform Payments)

Charge Types:

TypeWho CreatesWebhook LocationUse Case
Direct ChargeConnected AccountConnect endpointMarketplace where seller owns relationship
Destination ChargePlatformPlatform endpointPlatform controls experience
Separate Charges & TransfersPlatformPlatform endpointMaximum flexibility

⚠️ CRITICAL: Direct Charge Webhook Gap

When using Direct Charge, checkout sessions are created ON the Connected Account, NOT the platform. Webhooks go to the Connect endpoint, not the platform endpoint!

Platform endpoint:  /webhooks/stripe        → Has general events ✓
Connect endpoint:   /webhooks/stripe/connect → MUST have checkout.session.completed! ✓

Connect Endpoint MUST Handle:

  • checkout.session.completed (CRITICAL for Direct Charge)
  • checkout.session.expired
  • account.updated
  • payout.paid / payout.failed

Quick Start

import stripe

stripe.api_key = "sk_test_..."

# Create a checkout session
session = stripe.checkout.Session.create(
    payment_method_types=['card'],
    line_items=[{
        'price_data': {
            'currency': 'usd',
            'product_data': {
                'name': 'Premium Subscription',
            },
            'unit_amount': 2000,  # $20.00
            'recurring': {
                'interval': 'month',
            },
        },
        'quantity': 1,
    }],
    mode='subscription',
    success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
    cancel_url='https://yourdomain.com/cancel',
)

# Redirect user to session.url
print(session.url)

Payment Implementation Patterns

Pattern 1: One-Time Payment (Hosted Checkout)

def create_checkout_session(amount, currency='usd'):
    """Create a one-time payment checkout session."""
    try:
        session = stripe.checkout.Session.create(
            payment_method_types=['card'],
            line_items=[{
                'price_data': {
                    'currency': currency,
                    'product_data': {
                        'name': 'Purchase',
                        'images': ['https://example.com/product.jpg'],
                    },
                    'unit_amount': amount,  # Amount in cents
                },
                'quantity': 1,
            }],
            mode='payment',
            success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
            cancel_url='https://yourdomain.com/cancel',
            metadata={
                'order_id': 'order_123',
                'user_id': 'user_456'
            }
        )
        return session
    except stripe.error.StripeError as e:
        # Handle error
        print(f"Stripe error: {e.user_message}")
        raise

Pattern 2: Custom Payment Intent Flow

def create_payment_intent(amount, currency='usd', customer_id=None):
    """Create a payment intent for custom checkout UI."""
    intent = stripe.PaymentIntent.create(
        amount=amount,
        currency=currency,
        customer=customer_id,
        automatic_payment_methods={
            'enabled': True,
        },
        metadata={
            'integration_check': 'accept_a_payment'
        }
    )
    return intent.client_secret  # Send to frontend

# Frontend (JavaScript)
"""
const stripe = Stripe('pk_test_...');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');

const {error, paymentIntent} = await stripe.confirmCardPayment(
    clientSecret,
    {
        payment_method: {
            card: cardElement,
            billing_details: {
                name: 'Customer Name'
            }
        }
    }
);

if (error) {
    // Handle error
} else if (paymentIntent.status === 'succeeded') {
    // Payment successful
}
"""

Pattern 3: Subscription Creation

def create_subscription(customer_id, price_id):
    """Create a subscription for a customer."""
    try:
        subscription = stripe.Subscription.create(
            customer=customer_id,
            items=[{'price': price_id}],
            payment_behavior='default_incomplete',
            payment_settings={'save_default_payment_method': 'on_subscription'},
            expand=['latest_invoice.payment_intent'],
        )

        return {
            'subscription_id': subscription.id,
            'client_secret': subscription.latest_invoice.payment_intent.client_secret
        }
    except stripe.error.StripeError as e:
        print(f"Subscription creation failed: {e}")
        raise

Pattern 4: Customer Portal

def create_customer_portal_session(customer_id):
    """Create a portal session for customers to manage subscriptions."""
    session = stripe.billing_portal.Session.create(
        customer=customer_id,
        return_url='https://yourdomain.com/account',
    )
    return session.url  # Redirect customer here

Webhook Handling

Secure Webhook Endpoint

from flask import Flask, request
import stripe

app = Flask(__name__)

endpoint_secret = 'whsec_...'

@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.data
    sig_header = request.headers.get('Stripe-Signature')

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError:
        # Invalid payload
        return 'Invalid payload', 400
    except stripe.error.SignatureVerificationError:
        # Invalid signature
        return 'Invalid signature', 400

    # Handle the event
    if event['type'] == 'payment_intent.succeeded':
        payment_intent = event['data']['object']
        handle_successful_payment(payment_intent)
    elif event['type'] == 'payment_intent.payment_failed':
        payment_intent = event['data']['object']
        handle_failed_payment(payment_intent)
    elif event['type'] == 'customer.subscription.deleted':
        subscription = event['data']['object']
        handle_subscription_canceled(subscription)

    return 'Success', 200

def handle_successful_payment(payment_intent):
    """Process successful payment."""
    customer_id = payment_intent.get('customer')
    amount = payment_intent['amount']
    metadata = payment_intent.get('metadata', {})

    # Update your database
    # Send confirmation email
    # Fulfill order
    print(f"Payment succeeded: {payment_intent['id']}")

def handle_failed_payment(payment_intent):
    """Handle failed payment."""
    error = payment_intent.get('last_payment_error', {})
    print(f"Payment failed: {error.get('message')}")
    # Notify customer
    # Update order status

def handle_subscription_canceled(subscription):
    """Handle subscription cancellation."""
    customer_id = subscription['customer']
    # Update user access
    # Send cancellation email
    print(f"Subscription canceled: {subscription['id']}")

Webhook Best Practices

import hashlib
import hmac

def verify_webhook_signature(payload, signature, secret):
    """Manually verify webhook signature."""
    expected_sig = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_sig)

def handle_webhook_idempotently(event_id, handler):
    """Ensure webhook is processed exactly once."""
    # Check if event already processed
    if is_event_processed(event_id):
        return

    # Process event
    try:
        handler()
        mark_event_processed(event_id)
    except Exception as e:
        log_error(e)
        # Stripe will retry failed webhooks
        raise

Customer Management

def create_customer(email, name, payment_method_id=None):
    """Create a Stripe customer."""
    customer = stripe.Customer.create(
        email=email,
        name=name,
        payment_method=payment_method_id,
        invoice_settings={
            'default_payment_method': payment_method_id
        } if payment_method_id else None,
        metadata={
            'user_id': '12345'
        }
    )
    return customer

def attach_payment_method(customer_id, payment_method_id):
    """Attach a payment method to a customer."""
    stripe.PaymentMethod.attach(
        payment_method_id,
        customer=customer_id
    )

    # Set as default
    stripe.Customer.modify(
        customer_id,
        invoice_settings={
            'default_payment_method': payment_method_id
        }
    )

def list_customer_payment_methods(customer_id):
    """List all payment methods for a customer."""
    payment_methods = stripe.PaymentMethod.list(
        customer=customer_id,
        type='card'
    )
    return payment_methods.data

Refund Handling

def create_refund(payment_intent_id, amount=None, reason=None):
    """Create a refund."""
    refund_params = {
        'payment_intent': payment_intent_id
    }

    if amount:
        refund_params['amount'] = amount  # Partial refund

    if reason:
        refund_params['reason'] = reason  # 'duplicate', 'fraudulent', 'requested_by_customer'

    refund = stripe.Refund.create(**refund_params)
    return refund

def handle_dispute(charge_id, evidence):
    """Update dispute with evidence."""
    stripe.Dispute.modify(
        charge_id,
        evidence={
            'customer_name': evidence.get('customer_name'),
            'customer_email_address': evidence.get('customer_email'),
            'shipping_documentation': evidence.get('shipping_proof'),
            'customer_communication': evidence.get('communication'),
        }
    )

Testing

# Use test mode keys
stripe.api_key = "sk_test_..."

# Test card numbers
TEST_CARDS = {
    'success': '4242424242424242',
    'declined': '4000000000000002',
    '3d_secure': '4000002500003155',
    'insufficient_funds': '4000000000009995'
}

def test_payment_flow():
    """Test complete payment flow."""
    # Create test customer
    customer = stripe.Customer.create(
        email="test@example.com"
    )

    # Create payment intent
    intent = stripe.PaymentIntent.create(
        amount=1000,
        currency='usd',
        customer=customer.id,
        payment_method_types=['card']
    )

    # Confirm with test card
    confirmed = stripe.PaymentIntent.confirm(
        intent.id,
        payment_method='pm_card_visa'  # Test payment method
    )

    assert confirmed.status == 'succeeded'

⚠️ Critical Production Patterns

1. 100% Promo Code Detection (WRONG vs CORRECT)

Common Mistake:

# ❌ WRONG - no_payment_required is for different scenarios!
if session.payment_status == 'no_payment_required':
    handle_free_checkout()

Correct Detection:

# ✅ CORRECT - 100% promo codes have: status=complete, payment_status=paid, amount_total=0
def is_100_percent_promo(session):
    """Detect 100% discount promo code checkout."""
    return (
        session.payment_status == 'paid' and
        session.amount_total == 0 and
        session.payment_intent is None  # No payment intent when $0
    )

# In webhook handler
if session.status == 'complete':
    if is_100_percent_promo(session):
        # Handle free checkout from promo code
        fulfill_order(session)
    else:
        # Normal paid checkout
        fulfill_order(session)

Key Insight: Stripe says "paid" even when amount is $0 from a promo code. The no_payment_required status is for different scenarios (like $0 invoices for metered billing).


2. Dual Confirmation Pattern (Webhook + Frontend)

Problem: Frontend verification alone fails when:

  • User closes browser before redirect
  • Network error during verify call
  • Web mode where payment happens in separate tab

Solution: Dual Confirmation Architecture

Payment Complete
      ↓
 ┌────┴────┐
 ↓         ↓
Webhook   Frontend
(async)   (polling)
 ↓         ↓
 └────┬────┘
      ↓
First one wins (idempotent)

Backend Implementation:

// Idempotent order confirmation - BOTH webhook and frontend call this
async function confirmPayment(sessionId: string): Promise<boolean> {
  // Atomic conditional update - only updates if still pending
  const result = await db
    .update(orders)
    .set({
      status: 'paid',
      paidAt: new Date(),
      updatedAt: new Date()
    })
    .where(
      and(
        eq(orders.stripeSessionId, sessionId),
        eq(orders.status, 'pending')  // CRITICAL: Only if still pending!
      )
    );

  if (result.changes === 0) {
    // Already processed by other path - that's OK!
    return false;
  }

  // We just confirmed it - now do post-payment work
  await decrementInventory(sessionId);
  await sendConfirmationEmail(sessionId);
  return true;
}

// Webhook endpoint
app.post('/webhooks/stripe', async (req, res) => {
  const event = stripe.webhooks.constructEvent(...);

  if (event.type === 'checkout.session.completed') {
    await confirmPayment(event.data.object.id);
  }

  res.json({ received: true });
});

// Frontend verify endpoint
app.get('/api/verify-payment/:sessionId', async (req, res) => {
  const session = await stripe.checkout.sessions.retrieve(req.params.sessionId);

  if (session.status === 'complete') {
    await confirmPayment(session.id);
    return res.json({ success: true });
  }

  res.json({ success: false, status: session.status });
});

Frontend with Exponential Backoff (React Native/Web):

async function verifyPaymentWithRetry(
  sessionId: string,
  attempts = 3,
  initialDelay = 1500
): Promise<boolean> {
  let delay = initialDelay;

  for (let i = 0; i < attempts; i++) {
    await sleep(delay);

    try {
      const result = await api.verifyPayment(sessionId);

      if (result.success) return true;

      if (result.status === 'pending') {
        // Still processing - increase delay and retry
        delay = Math.min(delay * 1.5, 5000);
        continue;
      }

      // Failed or expired
      return false;
    } catch (error) {
      // Network error - retry
      delay = Math.min(delay * 1.5, 5000);
    }
  }

  return false;
}

3. Idempotency for All Payment Operations

Problem: Webhook and frontend can race, causing:

  • Double inventory/slot decrements
  • Duplicate notifications
  • Inconsistent state

Solution: Conditional UPDATE Pattern

-- Only update if still in expected state
UPDATE orders
SET status = 'paid', updated_at = NOW()
WHERE id = $1 AND status = 'pending';

-- Check affected rows
-- If 0 rows affected → another process already handled it → skip side effects

TypeScript/Drizzle Implementation:

async function processPaymentIdempotently(orderId: string) {
  const result = await db
    .update(orders)
    .set({ status: 'paid', updatedAt: new Date() })
    .where(and(
      eq(orders.id, orderId),
      eq(orders.status, 'pending')
    ));

  if (result.changes === 0) {
    // Already processed - safe to skip
    console.log(`Order ${orderId} already processed`);
    return { alreadyProcessed: true };
  }

  // We just confirmed - NOW do side effects
  await decrementInventory(orderId);
  await sendEmail(orderId);

  return { alreadyProcessed: false };
}

4. Web Browser Payment Flow (React Native/Expo)

Problem: WebBrowser.openBrowserAsync behaves DIFFERENTLY on web vs native!

PlatformReturn Timingresult.typeUser State
iOS/AndroidAfter browser closed'dismiss' or 'cancel'Back in app
WebImmediately'opened'Still viewing Stripe checkout!

⚠️ CRITICAL: On web, you CAN'T verify payment immediately because the user is still looking at Stripe checkout in another tab!

Correct Solution - Platform-Specific Handling:

import * as WebBrowser from 'expo-web-browser';
import { Platform, Alert } from 'react-native';

async function handlePayment(checkoutUrl: string, sessionId: string) {
  const result = await WebBrowser.openBrowserAsync(checkoutUrl);

  // Platform-specific handling based on result.type
  switch (result.type) {
    case 'cancel':
      // Native only: User explicitly cancelled (X button)
      // Don't verify - they cancelled intentionally
      Alert.alert('Payment Cancelled', 'You cancelled the payment.');
      break;

    case 'dismiss':
      // Native only: Browser was closed (could be success or cancel)
      // NOW it's safe to verify - user is back in the app
      const success = await verifyPaymentWithRetry(sessionId);
      if (success) {
        navigation.navigate('PaymentSuccess');
      } else {
        navigation.navigate('PaymentPending');
      }
      break;

    case 'opened':
      // WEB ONLY: Browser opened but user is STILL VIEWING STRIPE!
      // Do NOT verify immediately - show dialog instead
      Alert.alert(
        'Complete Your Payment',
        'Please complete your payment in the browser tab, then return here.',
        [
          {
            text: 'I\'ve Completed Payment',
            onPress: async () => {
              const success = await verifyPaymentWithRetry(sessionId);
              if (success) {
                navigation.navigate('PaymentSuccess');
              } else {
                Alert.alert('Payment Not Found', 'We couldn\'t confirm your payment. Please try again or contact support.');
              }
            }
          },
          {
            text: 'Cancel',
            style: 'cancel'
          }
        ]
      );
      break;
  }
}

Alternative for Web: Use Window Focus Event

// Web-specific: Listen for when user returns to tab
if (Platform.OS === 'web') {
  const handleFocus = async () => {
    window.removeEventListener('focus', handleFocus);
    // User returned to our tab - now verify
    const success = await verifyPaymentWithRetry(sessionId);
    // Handle result...
  };
  window.addEventListener('focus', handleFocus);
}

Verification with Exponential Backoff:

async function verifyPaymentWithRetry(
  sessionId: string,
  attempts = 3,
  initialDelay = 1500
): Promise<boolean> {
  let delay = initialDelay;

  for (let i = 0; i < attempts; i++) {
    await sleep(delay);

    try {
      const result = await api.verifyPayment(sessionId);

      if (result.success) return true;

      if (result.status === 'pending') {
        // Still processing - increase delay and retry
        delay = Math.min(delay * 1.5, 5000);
        continue;
      }

      // Failed or expired
      return false;
    } catch (error) {
      // Network error - retry
      delay = Math.min(delay * 1.5, 5000);
    }
  }

  return false;
}

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

5. Inventory/Slot Management with Atomicity

Rule: ONLY modify inventory AFTER payment confirmed, and atomically.

Problem Pattern (DON'T DO):

// ❌ WRONG - Decrementing before payment confirmation
await reserveSlot(slotId);  // Slot decremented
const session = await createCheckoutSession();  // Payment might fail!
// If user abandons → slot is stuck as reserved

Correct Pattern:

// ✅ CORRECT - Only decrement AFTER payment confirmed
async function confirmBookingPayment(sessionId: string) {
  // Atomic update with inventory in single transaction
  const result = await db.transaction(async (tx) => {
    // 1. Mark order as paid (only if pending)
    const orderUpdate = await tx
      .update(orders)
      .set({ status: 'paid' })
      .where(and(
        eq(orders.stripeSessionId, sessionId),
        eq(orders.status, 'pending')
      ));

    if (orderUpdate.changes === 0) {
      return { success: false, reason: 'already_processed' };
    }

    // 2. Get order details
    const order = await tx.query.orders.findFirst({
      where: eq(orders.stripeSessionId, sessionId)
    });

    // 3. Decrement inventory atomically
    const slotUpdate = await tx
      .update(slots)
      .set({
        availableCount: sql`available_count - 1`,
        updatedAt: new Date()
      })
      .where(and(
        eq(slots.id, order.slotId),
        gt(slots.availableCount, 0)  // Prevent negative
      ));

    if (slotUpdate.changes === 0) {
      // Slot became unavailable - need to refund
      throw new Error('SLOT_UNAVAILABLE');
    }

    return { success: true };
  });

  return result;
}

6. Stripe Connect Direct Charge Webhook Setup

Complete Connect Webhook Handler:

// /webhooks/stripe/connect - For Direct Charge events
app.post('/webhooks/stripe/connect',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['stripe-signature'];
    const connectWebhookSecret = process.env.STRIPE_CONNECT_WEBHOOK_SECRET!;

    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig!,
        connectWebhookSecret  // Different secret from platform webhook!
      );
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // Get the connected account ID
    const connectedAccountId = event.account;

    switch (event.type) {
      case 'checkout.session.completed':
        // CRITICAL: This is where Direct Charge sessions complete!
        await handleConnectCheckoutComplete(event.data.object, connectedAccountId);
        break;

      case 'checkout.session.expired':
        await handleConnectCheckoutExpired(event.data.object, connectedAccountId);
        break;

      case 'account.updated':
        await handleAccountUpdated(event.data.object);
        break;

      case 'payout.paid':
        await handlePayoutPaid(event.data.object, connectedAccountId);
        break;

      case 'payout.failed':
        await handlePayoutFailed(event.data.object, connectedAccountId);
        break;
    }

    res.json({ received: true });
  }
);

async function handleConnectCheckoutComplete(session, connectedAccountId: string) {
  // Retrieve full session with line items
  const fullSession = await stripe.checkout.sessions.retrieve(
    session.id,
    { expand: ['line_items'] },
    { stripeAccount: connectedAccountId }  // CRITICAL: Specify account!
  );

  // Confirm payment in your system
  await confirmPayment(fullSession.id);
}

Stripe Dashboard Setup Required:

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Add endpoint for Connect: https://yourdomain.com/webhooks/stripe/connect
  3. Select "Connected accounts" (NOT "Account")
  4. Add events: checkout.session.completed, checkout.session.expired, account.updated, payout.paid, payout.failed

Pre-Implementation Checklist

Webhook Setup

  • Platform endpoint handles platform events
  • Connect endpoint handles checkout.session.completed (if using Direct Charge)
  • Stripe Dashboard has Connect webhook with correct events
  • Webhook secrets configured for BOTH endpoints (different secrets!)

Payment Verification

  • Webhook handler implemented (primary - async, reliable)
  • Frontend verify endpoint implemented (secondary - immediate UX)
  • Both use conditional UPDATE for idempotency
  • 100% promo detected by amount_total === 0 (NOT no_payment_required)
  • Web vs Native browser handling: Check result.type === 'opened' (web) vs 'dismiss'/'cancel' (native) - do NOT verify immediately on web!

Inventory/Booking

  • Inventory only modified AFTER payment confirmed
  • Atomic operations prevent double-counting
  • Proper error handling if slot becomes unavailable (refund flow)

Testing

  • Test with regular payment
  • Test with 100% promo code
  • Test browser close during payment
  • Test network failure during verify
  • Verify webhook receives events from Connect accounts (if applicable)

Best Practices

  1. Always Use Webhooks: Don't rely solely on client-side confirmation
  2. Idempotency: Handle webhook events idempotently
  3. Error Handling: Gracefully handle all Stripe errors
  4. Test Mode: Thoroughly test with test keys before production
  5. Metadata: Use metadata to link Stripe objects to your database
  6. Monitoring: Track payment success rates and errors
  7. PCI Compliance: Never handle raw card data on your server
  8. SCA Ready: Implement 3D Secure for European payments

Common Pitfalls

  • Not Verifying Webhooks: Always verify webhook signatures
  • Missing Webhook Events: Handle all relevant webhook events
  • Hardcoded Amounts: Use cents/smallest currency unit
  • No Retry Logic: Implement retries for API calls
  • Ignoring Test Mode: Test all edge cases with test cards

平台分布

Claude Code

28.44%
按下载量换算32

Antigravity

21.07%
按下载量换算24

Cursor

18.23%
按下载量换算21

Gemini CLI

12.39%
按下载量换算14

windsurf

8.53%
按下载量换算10

OpenCode

3.48%
按下载量换算4

安全审计

Gen Agent Trust Hub

未通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在提醒或未通过项,安装前请自行复核。

继续浏览同类 Skills