Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Next.js i Stripe - Kompletny system płatności

W futurystycznej Metropolii Quantum, gdzie transakcje odbywają się błyskawicznie i bezpiecznie, poznasz integrację Next.js ze Stripe - najpopularniejszą platformą płatności online. Ta kombinacja pozwala budować profesjonalne systemy e-commerce z pełną obsługą płatności.

Czym jest Stripe?

Stripe to kompleksowa platforma płatności online, która oferuje:

  • Globalne płatności - obsługa 135+ walut i metod płatności
  • Developer-friendly API - czytelna dokumentacja i SDK
  • Security - zgodność z PCI DSS Level 1
  • Subscription management - zarządzanie subskrypcjami
  • Fraud prevention - zaawansowana ochrona przed oszustwami
  • Webhooks - real-time powiadomienia o zdarzeniach

Instalacja i konfiguracja

Krok 1: Utworzenie konta Stripe

  1. Zarejestruj się na stripe.com
  2. Przejdź do Dashboard → Developers → API keys
  3. Skopiuj klucze:
    • Publishable key - używany po stronie klienta
    • Secret key - używany tylko po stronie serwera

Krok 2: Instalacja pakietów

1# Zainstaluj Stripe SDK
2npm install stripe @stripe/stripe-js @stripe/react-stripe-js
3
4# Dla TypeScript dodaj typy
5npm install -D @types/stripe

Krok 3: Konfiguracja zmiennych środowiskowych

Utwórz plik

.env.local
:

1# Stripe keys
2NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
3STRIPE_SECRET_KEY=sk_test_...
4STRIPE_WEBHOOK_SECRET=whsec_...
5
6# App URL
7NEXT_PUBLIC_APP_URL=http://localhost:3000

Podstawowa integracja

Krok 1: Inicjalizacja Stripe

Utwórz instancję Stripe po stronie serwera:

1// lib/stripe.ts
2import Stripe from 'stripe';
3
4if (!process.env.STRIPE_SECRET_KEY) {
5  throw new Error('STRIPE_SECRET_KEY is not defined');
6}
7
8export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
9  apiVersion: '2023-10-16',
10  typescript: true,
11});

Po stronie klienta:

1// lib/stripe-client.ts
2import { loadStripe } from '@stripe/stripe-js';
3
4if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
5  throw new Error('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not defined');
6}
7
8// Singleton pattern dla lepszej wydajności
9let stripePromise: Promise<any>;
10
11export const getStripe = () => {
12  if (!stripePromise) {
13    stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
14  }
15  return stripePromise;
16};

Krok 2: Stripe Provider

Opakuj aplikację w Stripe Provider:

1// app/providers.tsx
2'use client';
3
4import { Elements } from '@stripe/react-stripe-js';
5import { getStripe } from '@/lib/stripe-client';
6
7interface ProvidersProps {
8  children: React.ReactNode;
9}
10
11export function Providers({ children }: ProvidersProps) {
12  return (
13    <Elements stripe={getStripe()}>
14      {children}
15    </Elements>
16  );
17}
18
19// app/layout.tsx
20import { Providers } from './providers';
21
22export default function RootLayout({
23  children,
24}: {
25  children: React.ReactNode;
26}) {
27  return (
28    <html lang="pl">
29      <body>
30        <Providers>
31          {children}
32        </Providers>
33      </body>
34    </html>
35  );
36}

Implementacja płatności

Checkout Session (Recommended)

Stripe Checkout to gotowy, hostowany formularz płatności:

1// app/api/checkout/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { stripe } from '@/lib/stripe';
4
5export async function POST(request: NextRequest) {
6  try {
7    const { items } = await request.json();
8
9    // Walidacja items
10    if (!items || !Array.isArray(items) || items.length === 0) {
11      return NextResponse.json(
12        { error: 'No items provided' },
13        { status: 400 }
14      );
15    }
16
17    // Tworzenie line items dla Stripe
18    const lineItems = items.map(item => ({
19      price_data: {
20        currency: 'pln',
21        product_data: {
22          name: item.name,
23          description: item.description,
24          images: item.images ? [item.images] : [],
25        },
26        unit_amount: Math.round(item.price * 100), // Stripe używa groszy
27      },
28      quantity: item.quantity,
29    }));
30
31    // Tworzenie sesji checkout
32    const session = await stripe.checkout.sessions.create({
33      payment_method_types: ['card', 'blik', 'p24'],
34      line_items: lineItems,
35      mode: 'payment',
36      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
37      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cart`,
38      metadata: {
39        orderId: 'unique-order-id',
40        customerId: 'customer-id',
41      },
42      locale: 'pl',
43    });
44
45    return NextResponse.json({ sessionId: session.id });
46  } catch (error) {
47    console.error('Checkout error:', error);
48    return NextResponse.json(
49      { error: 'Internal server error' },
50      { status: 500 }
51    );
52  }
53}

Komponent do obsługi checkout:

1// components/CheckoutButton.tsx
2'use client';
3
4import { useState } from 'react';
5import { useRouter } from 'next/navigation';
6import { getStripe } from '@/lib/stripe-client';
7
8interface CheckoutButtonProps {
9  items: Array<{
10    name: string;
11    description?: string;
12    price: number;
13    quantity: number;
14    images?: string;
15  }>;
16}
17
18export function CheckoutButton({ items }: CheckoutButtonProps) {
19  const router = useRouter();
20  const [loading, setLoading] = useState(false);
21
22  const handleCheckout = async () => {
23    setLoading(true);
24
25    try {
26      // Tworzenie sesji checkout
27      const response = await fetch('/api/checkout', {
28        method: 'POST',
29        headers: {
30          'Content-Type': 'application/json',
31        },
32        body: JSON.stringify({ items }),
33      });
34
35      const { sessionId, error } = await response.json();
36
37      if (error) {
38        throw new Error(error);
39      }
40
41      // Przekierowanie do Stripe Checkout
42      const stripe = await getStripe();
43      const { error: stripeError } = await stripe!.redirectToCheckout({
44        sessionId,
45      });
46
47      if (stripeError) {
48        throw stripeError;
49      }
50    } catch (error) {
51      console.error('Checkout error:', error);
52      alert('Wystąpił błąd podczas przekierowania do płatności');
53    } finally {
54      setLoading(false);
55    }
56  };
57
58  return (
59    <button
60      onClick={handleCheckout}
61      disabled={loading}
62      className="bg-blue-600 text-white px-6 py-3 rounded-lg disabled:opacity-50"
63    >
64      {loading ? 'Przekierowywanie...' : 'Przejdź do płatności'}
65    </button>
66  );
67}

Payment Element (Custom Form)

Dla większej kontroli nad UX, użyj Payment Element:

1// components/PaymentForm.tsx
2'use client';
3
4import { useState, FormEvent } from 'react';
5import {
6  PaymentElement,
7  useStripe,
8  useElements,
9} from '@stripe/react-stripe-js';
10
11interface PaymentFormProps {
12  clientSecret: string;
13}
14
15export function PaymentForm({ clientSecret }: PaymentFormProps) {
16  const stripe = useStripe();
17  const elements = useElements();
18  const [error, setError] = useState<string | null>(null);
19  const [processing, setProcessing] = useState(false);
20
21  const handleSubmit = async (e: FormEvent) => {
22    e.preventDefault();
23
24    if (!stripe || !elements) {
25      return;
26    }
27
28    setProcessing(true);
29    setError(null);
30
31    const { error: submitError } = await stripe.confirmPayment({
32      elements,
33      confirmParams: {
34        return_url: `${window.location.origin}/payment-success`,
35      },
36    });
37
38    if (submitError) {
39      setError(submitError.message || 'Wystąpił błąd');
40      setProcessing(false);
41    }
42  };
43
44  return (
45    <form onSubmit={handleSubmit} className="space-y-6">
46      <PaymentElement
47        options={{
48          layout: 'tabs',
49          paymentMethodOrder: ['card', 'blik', 'p24'],
50        }}
51      />
52      
53      {error && (
54        <div className="text-red-600 text-sm">{error}</div>
55      )}
56      
57      <button
58        type="submit"
59        disabled={!stripe || processing}
60        className="w-full bg-blue-600 text-white py-3 rounded-lg disabled:opacity-50"
61      >
62        {processing ? 'Przetwarzanie...' : 'Zapłać'}
63      </button>
64    </form>
65  );
66}

API dla Payment Intent

1// app/api/payment-intent/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { stripe } from '@/lib/stripe';
4
5export async function POST(request: NextRequest) {
6  try {
7    const { amount, currency = 'pln' } = await request.json();
8
9    // Walidacja kwoty
10    if (!amount || amount <= 0) {
11      return NextResponse.json(
12        { error: 'Invalid amount' },
13        { status: 400 }
14      );
15    }
16
17    // Tworzenie Payment Intent
18    const paymentIntent = await stripe.paymentIntents.create({
19      amount: Math.round(amount * 100), // Konwersja na grosze
20      currency,
21      automatic_payment_methods: {
22        enabled: true,
23      },
24      metadata: {
25        orderId: 'unique-order-id',
26      },
27    });
28
29    return NextResponse.json({
30      clientSecret: paymentIntent.client_secret,
31    });
32  } catch (error) {
33    console.error('Payment intent error:', error);
34    return NextResponse.json(
35      { error: 'Failed to create payment intent' },
36      { status: 500 }
37    );
38  }
39}

Webhooks

Webhooks pozwalają na otrzymywanie powiadomień o zdarzeniach w Stripe:

1// app/api/webhooks/stripe/route.ts
2import { headers } from 'next/headers';
3import { NextRequest, NextResponse } from 'next/server';
4import { stripe } from '@/lib/stripe';
5import Stripe from 'stripe';
6
7export async function POST(request: NextRequest) {
8  const body = await request.text();
9  const signature = (await headers()).get('stripe-signature');
10
11  if (!signature) {
12    return NextResponse.json(
13      { error: 'Missing stripe-signature header' },
14      { status: 400 }
15    );
16  }
17
18  let event: Stripe.Event;
19
20  try {
21    event = stripe.webhooks.constructEvent(
22      body,
23      signature,
24      process.env.STRIPE_WEBHOOK_SECRET!
25    );
26  } catch (error) {
27    console.error('Webhook signature verification failed:', error);
28    return NextResponse.json(
29      { error: 'Invalid signature' },
30      { status: 400 }
31    );
32  }
33
34  // Obsługa różnych typów eventów
35  try {
36    switch (event.type) {
37      case 'checkout.session.completed': {
38        const session = event.data.object as Stripe.Checkout.Session;
39        
40        // Obsłuż ukończoną płatność
41        await handleSuccessfulPayment(session);
42        break;
43      }
44
45      case 'payment_intent.succeeded': {
46        const paymentIntent = event.data.object as Stripe.PaymentIntent;
47        
48        // Aktualizuj status zamówienia
49        console.log('Payment succeeded:', paymentIntent.id);
50        break;
51      }
52
53      case 'payment_intent.payment_failed': {
54        const paymentIntent = event.data.object as Stripe.PaymentIntent;
55        
56        // Obsłuż nieudaną płatność
57        console.log('Payment failed:', paymentIntent.id);
58        break;
59      }
60
61      case 'customer.subscription.created':
62      case 'customer.subscription.updated':
63      case 'customer.subscription.deleted': {
64        const subscription = event.data.object as Stripe.Subscription;
65        
66        // Obsłuż zmiany w subskrypcji
67        await handleSubscriptionChange(subscription);
68        break;
69      }
70
71      default:
72        console.log(`Unhandled event type: ${event.type}`);
73    }
74
75    return NextResponse.json({ received: true });
76  } catch (error) {
77    console.error('Webhook handler error:', error);
78    return NextResponse.json(
79      { error: 'Webhook handler failed' },
80      { status: 500 }
81    );
82  }
83}
84
85async function handleSuccessfulPayment(session: Stripe.Checkout.Session) {
86  // Implementacja logiki po udanej płatności
87  console.log('Payment successful for session:', session.id);
88  
89  // Przykład: Aktualizacja statusu zamówienia w bazie danych
90  // await updateOrderStatus(session.metadata.orderId, 'paid');
91  
92  // Wysłanie emaila z potwierdzeniem
93  // await sendConfirmationEmail(session.customer_email);
94}
95
96async function handleSubscriptionChange(subscription: Stripe.Subscription) {
97  // Implementacja logiki dla subskrypcji
98  console.log('Subscription changed:', subscription.id);
99}

Subskrypcje

Implementacja systemu subskrypcji:

1// app/api/subscription/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { stripe } from '@/lib/stripe';
4import { auth } from '@/lib/auth'; // Twoja funkcja autentykacji
5
6export async function POST(request: NextRequest) {
7  try {
8    const session = await auth();
9    if (!session?.user?.email) {
10      return NextResponse.json(
11        { error: 'Unauthorized' },
12        { status: 401 }
13      );
14    }
15
16    const { priceId } = await request.json();
17
18    // Znajdź lub utwórz klienta w Stripe
19    let customerId: string;
20    
21    const customers = await stripe.customers.list({
22      email: session.user.email,
23      limit: 1,
24    });
25
26    if (customers.data.length > 0) {
27      customerId = customers.data[0].id;
28    } else {
29      const customer = await stripe.customers.create({
30        email: session.user.email,
31        metadata: {
32          userId: session.user.id,
33        },
34      });
35      customerId = customer.id;
36    }
37
38    // Utwórz sesję checkout dla subskrypcji
39    const checkoutSession = await stripe.checkout.sessions.create({
40      customer: customerId,
41      payment_method_types: ['card'],
42      line_items: [
43        {
44          price: priceId,
45          quantity: 1,
46        },
47      ],
48      mode: 'subscription',
49      allow_promotion_codes: true,
50      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
51      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
52      metadata: {
53        userId: session.user.id,
54      },
55    });
56
57    return NextResponse.json({ sessionId: checkoutSession.id });
58  } catch (error) {
59    console.error('Subscription error:', error);
60    return NextResponse.json(
61      { error: 'Failed to create subscription' },
62      { status: 500 }
63    );
64  }
65}

Customer Portal

Pozwól użytkownikom zarządzać subskrypcjami:

1// app/api/customer-portal/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { stripe } from '@/lib/stripe';
4import { auth } from '@/lib/auth';
5
6export async function POST(request: NextRequest) {
7  try {
8    const session = await auth();
9    if (!session?.user?.email) {
10      return NextResponse.json(
11        { error: 'Unauthorized' },
12        { status: 401 }
13      );
14    }
15
16    // Znajdź klienta
17    const customers = await stripe.customers.list({
18      email: session.user.email,
19      limit: 1,
20    });
21
22    if (customers.data.length === 0) {
23      return NextResponse.json(
24        { error: 'Customer not found' },
25        { status: 404 }
26      );
27    }
28
29    // Utwórz sesję Customer Portal
30    const portalSession = await stripe.billingPortal.sessions.create({
31      customer: customers.data[0].id,
32      return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
33    });
34
35    return NextResponse.json({ url: portalSession.url });
36  } catch (error) {
37    console.error('Customer portal error:', error);
38    return NextResponse.json(
39      { error: 'Failed to create portal session' },
40      { status: 500 }
41    );
42  }
43}

Bezpieczeństwo

1. Walidacja po stronie serwera

Zawsze waliduj dane po stronie serwera:

1// lib/validation.ts
2import { z } from 'zod';
3
4export const checkoutSchema = z.object({
5  items: z.array(z.object({
6    name: z.string().min(1).max(100),
7    description: z.string().optional(),
8    price: z.number().positive(),
9    quantity: z.number().int().positive(),
10    images: z.string().url().optional(),
11  })).min(1),
12});
13
14// Użycie w API route
15const validatedData = checkoutSchema.parse(await request.json());

2. Rate limiting

Zabezpiecz API przed nadużyciami:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { Ratelimit } from '@upstash/ratelimit';
5import { Redis } from '@upstash/redis';
6
7const ratelimit = new Ratelimit({
8  redis: Redis.fromEnv(),
9  limiter: Ratelimit.slidingWindow(10, '10 s'),
10});
11
12export async function middleware(request: NextRequest) {
13  if (request.nextUrl.pathname.startsWith('/api/')) {
14    const ip = request.ip ?? '127.0.0.1';
15    const { success } = await ratelimit.limit(ip);
16    
17    if (!success) {
18      return NextResponse.json(
19        { error: 'Too many requests' },
20        { status: 429 }
21      );
22    }
23  }
24  
25  return NextResponse.next();
26}

3. Webhook security

Zawsze weryfikuj podpisy webhooków:

1// Użyj stripe.webhooks.constructEvent() jak pokazano wcześniej
2// Dodatkowo możesz dodać IP whitelisting
3const allowedIPs = [
4  '3.18.12.63',
5  '3.130.192.231',
6  // Pełna lista: https://stripe.com/docs/ips
7];

Testing

Test mode

Stripe oferuje tryb testowy z kartami testowymi:

1// Popularne karty testowe
2const testCards = {
3  success: '4242424242424242',
4  decline: '4000000000000002',
5  authRequired: '4000002500003155',
6  insufficientFunds: '4000000000009995',
7};

Testowanie webhooków lokalnie

Użyj Stripe CLI:

1# Instalacja
2brew install stripe/stripe-cli/stripe
3
4# Logowanie
5stripe login
6
7# Forward webhooks do lokalnego serwera
8stripe listen --forward-to localhost:3000/api/webhooks/stripe
9
10# Test webhook
11stripe trigger payment_intent.succeeded

Podsumowanie

Integracja Next.js ze Stripe daje potężne możliwości budowania systemów płatności. Kluczowe elementy to:

  1. Checkout Session - dla szybkiej implementacji
  2. Payment Element - dla custom UX
  3. Webhooks - dla niezawodności
  4. Security - walidacja i rate limiting
  5. Testing - tryb testowy i Stripe CLI

Ta kombinacja pozwala budować profesjonalne aplikacje e-commerce, SaaS z subskrypcjami, marketplace i wiele innych.

Vai a CodeWorlds