4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

watnow Advent Calendar 2024

Day 8

Stripeで決済機能をつくる

Last updated at Posted at 2024-12-08

はじめに

今回、watnowアドベントカレンダー8日目を担当させていただきます。
この記事では、StripeのAPIを用いたシンプルな決済処理を実装していきます。

使用するツールや言語

  • Next.js
  • MUI
  • Stripe

実装するもの

名前、値段選択ができるフォームからStripeを用いて決済処理を行う。

フォルダ構造

.
├── pages/
│   ├── api/
│   │   └── create-payment-intent.ts
│   ├── settlement.tsx
│   └── checkoutForm.tsx
└── .env.local

実装

Stripeのセットアップ

新規登録

Stripeのアカウントを作成します。
リンクはこちら

スクリーンショット (23).png

keyの取得

アカウント設定が終わると、新しいビジネスとして下のような管理者ページに遷移すると思います。今回はこのテスト環境で実装していきます。

スクリーンショット (25).png

上記の画像の右にある公開可能キー、シークレットキーをコピーしてenvファイルに保存しておいてください。

.env.local
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= "pk_test_***************************************"
STRIPE_SECRET_KEY="sk_test_*********************************************"

API

Stripeのインストール

npm install stripe

create-payment-intent.ts

create-payment-intent.ts
import { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", { apiVersion: '2024-11-20.acacia' });

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method !== 'POST') {
        return res.status(405).json({ error: 'Method not allowed' });
    }

    const { amount } = req.body;
    if (!amount) {
        return res.status(400).json({ error: 'Amount is required' });
    }

    try {
        const paymentIntent = await stripe.paymentIntents.create({
            amount: parseInt(amount, 10),
            currency: 'jpy',
        });

        res.status(200).json({ clientSecret: paymentIntent.client_secret });
    } catch (error) {
        res.status(500).json({ error: (error as Error).message });
    }
}

17行目のpaymentIntentではamount、currencyの値は必ず指定する必要があります。詳しくはこちらを参照してください。

フロントエンド

Stripeのインストール

npm install @stripe/react-stripe-js @stripe/stripe-js

MUIのインストール

npm install @mui/material @emotion/react @emotion/styled

settlement.tsx

settlement.tsx
import React, { useEffect, useState } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { CheckoutForm } from './checkoutForm';
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { Box, TextField, MenuItem, Select, FormControl, InputLabel } from '@mui/material';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "");

export default function Settlement() {
    const [clientSecret, setClientSecret] = useState<string>("");
    const [amount, setAmount] = useState<string>("");
    const [name, setName] = useState<string>("");

    useEffect(() => {
        const createPaymentIntent = async () => {
            if (!amount) return;
            const response = await fetch('/api/create-payment-intent', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ amount }),
            });
            const data = await response.json();
            setClientSecret(data.clientSecret);
        };
        createPaymentIntent();
    }, [amount]);

    const options: StripeElementsOptions = {
        appearance: { theme: 'stripe' },
        clientSecret,
    };

    return (
        <Box sx={{ p: 3, backgroundColor: '#0A2540', color: 'white', minHeight: '100vh' }}>
            <TextField
                fullWidth
                label="名前"
                value={name}
                onChange={(e) => setName(e.target.value)}
                sx={{ mb: 3 }}
                InputLabelProps={{ sx: { color: 'white' } }}
                InputProps={{ sx: { color: 'white' } }}
            />
            <FormControl fullWidth sx={{ mb: 3 }}>
                <InputLabel id="amount-label" sx={{ color: 'white' }}>値段</InputLabel>
                <Select
                    labelId="amount-label"
                    value={amount}
                    onChange={(e) => setAmount(e.target.value as string)}
                    displayEmpty
                    sx={{ backgroundColor: '#212D63', color: 'white', '.MuiSelect-icon': { color: 'white' } }}
                >
                    <MenuItem value=""></MenuItem>
                    <MenuItem value="5000">5,000円</MenuItem>
                    <MenuItem value="10000">10,000円</MenuItem>
                    <MenuItem value="30000">30,000円</MenuItem>
                </Select>
            </FormControl>

            {clientSecret && (
                <Elements stripe={stripePromise} options={options}>
                    <CheckoutForm name={name} amount={Number(amount)} clientSecret={clientSecret} />
                </Elements>
            )}
        </Box>
    );
}

loadStripe : Stripeオブジェクトを非同期で読込み、初期化する関数です。公開可能キーを渡すことに注意してください。
StripeElementsOptions : カード情報を入力するフォームのカスタマイズを行うことができます。

checkoutForm.tsx

checkoutForm.tsx
import React, { useState } from 'react';
import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js';
import { Box, Button } from '@mui/material';

interface CheckoutFormProps {
    name: string;
    amount: number;
    clientSecret: string;
}

export const CheckoutForm: React.FC<CheckoutFormProps> = ({ name, amount, clientSecret }) => {
    const stripe = useStripe();
    const elements = useElements();
    const [loading, setLoading] = useState(false);

    const handleSubmit = async (event: React.FormEvent) => {
        event.preventDefault();
        if (!stripe || !elements || !clientSecret) {
            console.error('Stripe, Elements, or clientSecret is not available.');
            return;
        }

        const cardElement = elements.getElement(CardElement);
        if (!cardElement) return;

        setLoading(true);
        
        const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
            payment_method: {
                card: cardElement,
                billing_details: { name },
            },
        });

        setLoading(false);

        if (error) {
            console.error('Error:', error);
            alert('支払いに失敗しました');
        } else if (paymentIntent?.status === 'succeeded') {
            alert('支払いが成功しました');
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <Box sx={{ mb: 3, p: 2, backgroundColor: '#212D63', borderRadius: '8px' }}>
                <CardElement options={{ style: { base: { color: 'white' } } }} />
            </Box>
            <Button 
                type="submit" 
                variant="contained" 
                color="primary" 
                fullWidth 
                disabled={!stripe || loading}
            >
                {loading ? '処理中...' : `支払う (${amount}円)`}
            </Button>
        </form>
    );
};

useStripe : 支払いのリクエストの送信に使います。
useElements : 入力したカード情報の取得に使います。
CardElement : カード情報入力フォームを作成します。

実行結果

スクリーンショット (26).png

上記のように名前、値段、カード情報を入力することができます。
ダミーカードに関してはこちらを参照して下さい。

スクリーンショット (27).png
stripeの管理者ページの取引の欄を見ると先ほどの決済情報が反映されていることが分かります。

参考資料

まとめ

最後まで読んでいただきありがとうございました。
少しでも参考になれば幸いです。
また、今回の実装では決済作成のAPIしか用いなかったので値段を変えた際に前の決済処理が未完了となり新しく決済が作成されてしまいます。
スクリーンショット (28).png

興味を持った方はupdateの部分の実装に挑戦してみてください!

4
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?