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

Bài 18: Email, Payment & Third-party Integration

Gửi email với Resend & React Email. Stripe payment integration. Checkout flow, subscription. Third-party SDK patterns.

💻 Lập trình — Bài 18 Bài 18: Email, Payment & Third-party Integration

React & Next.js: Từ Cơ bản đến Nâng cao

Phần 5: Full-Stack Features

xdev.asia

1. Email với Resend

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. Stripe Payment Setup

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. Checkout Flow

// 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. Subscription Management

// 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 };
}

Bài tiếp theo: Testing React & Next.js — unit test, integration test, E2E.