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.
Stripe to kompleksowa platforma płatności online, która oferuje:
1# Zainstaluj Stripe SDK
2npm install stripe @stripe/stripe-js @stripe/react-stripe-js
3
4# Dla TypeScript dodaj typy
5npm install -D @types/stripeUtwó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:3000Utwó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};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}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}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}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 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}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}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}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());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}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];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};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.succeededIntegracja Next.js ze Stripe daje potężne możliwości budowania systemów płatności. Kluczowe elementy to:
Ta kombinacja pozwala budować profesjonalne aplikacje e-commerce, SaaS z subskrypcjami, marketplace i wiele innych.