Astro กับ Stripe: รับชำระเงินออนไลน์

#astro13 เม.ย. 2569

Astro กับ Stripe: รับชำระเงินออนไลน์

Stripe เป็น payment platform ที่ได้รับความนิยมสูงสุดในโลก ด้วย API ที่ครบครันและ documentation ที่ยอดเยี่ยม การผสาน Stripe เข้ากับ Astro ทำให้คุณสามารถรับชำระเงินออนไลน์ได้อย่างปลอดภัยและมีประสิทธิภาพ

การติดตั้ง

npm install stripe @stripe/stripe-js

ตั้งค่า environment variables:

# .env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

สร้าง Stripe Client

// src/lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-06-20',
  typescript: true,
});

export const STRIPE_PUBLISHABLE_KEY = import.meta.env.STRIPE_PUBLISHABLE_KEY;

สร้าง Checkout Session

// src/pages/api/checkout.ts
import type { APIRoute } from 'astro';
import { stripe } from '../../lib/stripe';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
  image?: string;
}

export const POST: APIRoute = async ({ request }) => {
  const { items, successUrl, cancelUrl } = await request.json() as {
    items: CartItem[];
    successUrl: string;
    cancelUrl: string;
  };

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card', 'promptpay'],
    line_items: items.map((item) => ({
      price_data: {
        currency: 'thb',
        product_data: {
          name: item.name,
          images: item.image ? [item.image] : [],
        },
        unit_amount: item.price * 100, // Stripe ใช้ satang
      },
      quantity: item.quantity,
    })),
    mode: 'payment',
    success_url: successUrl,
    cancel_url: cancelUrl,
    locale: 'th',
    metadata: {
      orderId: crypto.randomUUID(),
    },
  });

  return new Response(
    JSON.stringify({ sessionId: session.id, url: session.url }),
    { headers: { 'Content-Type': 'application/json' } }
  );
};

Checkout Page

---
// src/pages/checkout.astro
import { STRIPE_PUBLISHABLE_KEY } from '../lib/stripe';
---

<html lang="th">
  <head>
    <title>ชำระเงิน</title>
    <script src="https://js.stripe.com/v3/" is:inline></script>
  </head>
  <body>
    <div id="checkout-container">
      <h1>สรุปคำสั่งซื้อ</h1>
      <div id="cart-items"></div>
      <button id="checkout-btn">ชำระเงิน</button>
    </div>

    <script define:vars={{ publishableKey: STRIPE_PUBLISHABLE_KEY }}>
      const stripe = Stripe(publishableKey);

      // ดึงข้อมูล cart จาก localStorage
      const cart = JSON.parse(localStorage.getItem('cart') || '[]');

      document.getElementById('checkout-btn')?.addEventListener('click', async () => {
        const response = await fetch('/api/checkout', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            items: cart,
            successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
            cancelUrl: `${window.location.origin}/cart`,
          }),
        });

        const { url } = await response.json();
        window.location.href = url;
      });
    </script>
  </body>
</html>

Webhook Handler

// src/pages/api/webhooks/stripe.ts
import type { APIRoute } from 'astro';
import { stripe } from '../../../lib/stripe';
import Stripe from 'stripe';

export const POST: APIRoute = async ({ request }) => {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      import.meta.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return new Response(`Webhook Error: ${err}`, { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleSuccessfulPayment(session);
      break;
    }
    case 'payment_intent.payment_failed': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      await handleFailedPayment(paymentIntent);
      break;
    }
    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionChange(subscription);
      break;
    }
  }

  return new Response(JSON.stringify({ received: true }), {
    headers: { 'Content-Type': 'application/json' },
  });
};

async function handleSuccessfulPayment(session: Stripe.Checkout.Session) {
  console.log('Payment successful:', session.id);
  // บันทึก order ลง database
  // ส่ง confirmation email
}

async function handleFailedPayment(paymentIntent: Stripe.PaymentIntent) {
  console.log('Payment failed:', paymentIntent.id);
  // แจ้งเตือน customer
}

async function handleSubscriptionChange(subscription: Stripe.Subscription) {
  console.log('Subscription changed:', subscription.id);
  // อัปเดต user subscription status
}

Subscription Plans

// src/pages/api/subscriptions/create.ts
import type { APIRoute } from 'astro';
import { stripe } from '../../../lib/stripe';

export const POST: APIRoute = async ({ request }) => {
  const { priceId, customerId } = await request.json();

  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    payment_behavior: 'default_incomplete',
    payment_settings: {
      save_default_payment_method: 'on_subscription',
    },
    expand: ['latest_invoice.payment_intent'],
  });

  const invoice = subscription.latest_invoice as any;
  const paymentIntent = invoice?.payment_intent;

  return new Response(
    JSON.stringify({
      subscriptionId: subscription.id,
      clientSecret: paymentIntent?.client_secret,
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );
};

Success Page

---
// src/pages/success.astro
import { stripe } from '../lib/stripe';

const sessionId = Astro.url.searchParams.get('session_id');

if (!sessionId) {
  return Astro.redirect('/');
}

const session = await stripe.checkout.sessions.retrieve(sessionId, {
  expand: ['line_items', 'customer'],
});
---

<html lang="th">
  <body>
    <h1>ชำระเงินสำเร็จ!</h1>
    <p>ขอบคุณสำหรับการสั่งซื้อ</p>
    <p>หมายเลขคำสั่งซื้อ: {session.metadata?.orderId}</p>
    <p>ยอดชำระ: {(session.amount_total ?? 0) / 100} บาท</p>
  </body>
</html>

สรุป

การผสาน Stripe กับ Astro ทำให้สามารถสร้างระบบรับชำระเงินที่ปลอดภัยและครบครันได้อย่างรวดเร็ว ตั้งแต่ one-time payments ไปจนถึง subscription plans ด้วย webhook handling ที่ถูกต้อง ทำให้ระบบ payment มีความน่าเชื่อถือสูง