第二章:Next.js 整合 Stripe Checkout

在這章中,我們將學習如何使用 Stripe 的大殺器:Stripe Checkout

如果你曾經串接過本土金流(如綠界),你會發現你需要自己刻一個結帳頁面,然後把資料打包、加密、轉址。而 Stripe Checkout 完全顛覆了這個流程。 Stripe Checkout 是一個由 Stripe 託管 (Hosted) 的結帳頁面,它經過了數十億次交易的 A/B 測試,擁有極高的轉換率。它不僅支援多國語系自動切換、貨幣在地化,更內建了 Apple PayGoogle Pay 的一鍵付款功能!

我們現在要做的,就是透過 Next.js 的後端 API,向 Stripe「申請」一個專屬的 Checkout URL,然後把使用者引導過去。


💻 實作:建立 Checkout Session API

在 App Router 架構下,我們建立一支 API 路由:src/app/api/stripe/checkout/route.ts

import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { createClient } from '@/utils/supabase/server'; // 假設你用 Supabase

// 初始化 Stripe 實例
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16', // 建議鎖定 API 版本
});

export async function POST(request: Request) {
  try {
    // 1. 驗證使用者身分 (確保只有登入會員可以購買)
    const supabase = await createClient();
    const { data: { user } } = await supabase.auth.getUser();

    if (!user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    // 2. 從前端取得要購買的 Price ID
    const body = await request.json();
    const { priceId } = body;

    // 3. 建立 Stripe Checkout Session
    // 這是最核心的 API 呼叫!
    const session = await stripe.checkout.sessions.create({
      // 告訴 Stripe 這次交易包含什麼商品
      line_items: [
        {
          price: priceId, // 傳入上一章我們在 Dashboard 建立的 Price ID
          quantity: 1,    // 數量為 1
        },
      ],
      // mode 可以是 'payment' (單次付款) 或是 'subscription' (訂閱制)
      mode: 'subscription', 
      
      // 成功與失敗的跳轉網址 (ReturnURL 的前端版本)
      // 注意:這裡必須使用環境變數來動態決定網址,避免部署後跳回 localhost!
      success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
      
      // 將我們資料庫的 User ID 偷藏在中繼資料 (metadata) 裡面
      // 這樣當付款成功時,Stripe Webhook 會把這個 ID 傳回來給我們
      metadata: {
        userId: user.id,
      },
      
      // 預先帶入使用者的 Email,省去他們填寫的時間,提升轉換率!
      customer_email: user.email,
    });

    // 4. 回傳建立好的 Checkout URL 給前端
    return NextResponse.json({ url: session.url });

  } catch (error: any) {
    console.error('Stripe Checkout Error:', error);
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

💡 程式碼亮點解析

  1. mode: 'subscription':只要這一行字,Stripe 就會自動幫你處理每個月定期扣款的邏輯,你完全不需要自己寫 Cron Job (排程) 去定期刷卡!如果你賣的是單次買斷的課程,只要改成 mode: 'payment' 即可。
  2. metadata: { userId: user.id }:這是一個極度重要的技巧!這就像是我們在綠界裡用的 CustomField。當使用者在 Stripe 頁面刷完卡,Stripe 會發一個 Webhook 給我們,我們必須靠這個 metadata.userId 才知道到底要幫資料庫裡的哪一個帳號開通權限。
  3. success_url:這就是使用者刷卡成功後會看到的頁面。Stripe 允許你在 URL 裡面加上 {CHECKOUT_SESSION_ID} 變數,Stripe 會自動把它替換成真實的 Session ID,讓你的前端頁面可以拿這個 ID 去要資料。

🖥️ 前端串接:發起結帳請求

後端 API 寫好後,前端的按鈕就非常簡單了。在你的 Pricing 頁面上:

'use client';
import { useState } from 'react';

export default function PricingButton({ priceId }: { priceId: string }) {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/stripe/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ priceId }),
      });
      
      const data = await response.json();
      
      if (data.url) {
        // 直接將瀏覽器導向 Stripe 的高轉換率結帳頁面
        window.location.href = data.url;
      }
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button 
      onClick={handleCheckout} 
      disabled={loading}
      className="bg-indigo-600 text-white font-bold py-3 px-6 rounded-xl hover:bg-indigo-700 transition"
    >
      {loading ? '準備結帳中...' : '使用 Apple Pay / 信用卡付款'}
    </button>
  );
}

✅ 本章小結

現在,只要使用者點擊按鈕,他們就會被絲滑地引導到 Stripe 的官方結帳頁面,並且能看見 Apple Pay 或 Google Pay 的選項! 但等等,使用者付款成功被跳轉回 success_url 之後,我們該怎麼把權限給他? 在下一章,我們將探討金流系統的靈魂:Webhook 伺服器對伺服器通訊。這將確保即使使用者的瀏覽器在付款當下崩潰,你的資料庫依然能準確紀錄這筆美金入帳!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!