Chuyển đến nội dung chính

第 18 課:電子郵件、付款和第三方集成

使用重新傳送和反應電子郵件傳送電子郵件。條紋支付整合​​。結帳流程、訂閱。第三方 SDK 模式。

💻 程式設計 — 第 18 課 第 18 課:電子郵件、付款和第三方 整合

React 和 Next.js:從基礎到高級

第 5 部分:全端功能

亞洲開發網

1. 重新發送電子郵件

npm install resend @react-email/components
// emails/welcome.tsx — React Email template
import { Html, Head, Body, Container, Heading, Text, Button } from '@react-email/components';

export function WelcomeEmail({ name, url }: { name: string; url: string }) {
  return (
    <Html>
      <Head />
      <Body style={{ fontFamily: 'sans-serif' }}>
        <Container>
          <Heading>Chào mừng, {name}!</Heading>
          <Text>Cảm ơn bạn đã đăng ký tài khoản.</Text>
          <Button href={url} style={{ background: '#0070f3', color: '#fff', padding: '12px 24px' }}>
            Xác nhận email
          </Button>
        </Container>
      </Body>
    </Html>
  );
}
// lib/email.ts
import { Resend } from 'resend';
import { WelcomeEmail } from '@/emails/welcome';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendWelcomeEmail(email: string, name: string) {
  await resend.emails.send({
    from: '[email protected]',
    to: email,
    subject: `Chào mừng ${name} đến với xDev!`,
    react: WelcomeEmail({ name, url: `https://blog.xdev.asia/verify?email=${email}` }),
  });
}

2. 條紋支付設置

npm install stripe @stripe/stripe-js @stripe/react-stripe-js
// lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
});

3. 結帳流程

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { auth } from '@/auth';

export async function POST(request: NextRequest) {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { priceId } = await request.json();

  const checkoutSession = await stripe.checkout.sessions.create({
    customer_email: session.user?.email!,
    mode: 'payment', // or 'subscription'
    payment_method_types: ['card'],
    line_items: [
      { price: priceId, quantity: 1 },
    ],
    success_url: `${process.env.NEXT_PUBLIC_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/payment/cancel`,
    metadata: {
      userId: session.user?.id!,
    },
  });

  return NextResponse.json({ url: checkoutSession.url });
}
// Client — Redirect to Stripe Checkout
'use client';

export function CheckoutButton({ priceId }: { priceId: string }) {
  const handleCheckout = async () => {
    const res = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ priceId }),
    });
    const { url } = await res.json();
    window.location.href = url;
  };

  return (
    <button onClick={handleCheckout}>
      Mua ngay
    </button>
  );
}

4.Stripe Webhook

// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
import { db } from '@/lib/db';

export async function POST(request: Request) {
  const body = await request.text();
  const headersList = await headers();
  const signature = headersList.get('stripe-signature')!;

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;
      await db.order.create({
        data: {
          userId: session.metadata?.userId!,
          stripeSessionId: session.id,
          amount: session.amount_total! / 100,
          status: 'completed',
        },
      });
      break;
    }
    case 'invoice.payment_failed': {
      const invoice = event.data.object;
      // Handle failed subscription payment
      break;
    }
  }

  return new Response('OK');
}

5. 訂閱管理

// Server Action — manage subscription
'use server';
import { stripe } from '@/lib/stripe';
import { auth } from '@/auth';
import { db } from '@/lib/db';

export async function createCustomerPortal() {
  const session = await auth();
  if (!session) throw new Error('Unauthorized');

  const user = await db.user.findUnique({
    where: { id: session.user?.id },
  });

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user!.stripeCustomerId!,
    return_url: `${process.env.NEXT_PUBLIC_URL}/settings/billing`,
  });

  return { url: portalSession.url };
}

下一篇: 測試 React 和 Next.js — 單元測試、整合測試、E2E。