Stripeで決済機能をつくる



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


### 使用するツールや言語

- Next.js
- MUI
- Stripe

### 実装するもの
名前、値段選択ができるフォームからStripeを用いて決済処理を行う。
### フォルダ構造
```
.
├── pages/
│   ├── api/
│   │   └── create-payment-intent.ts
│   ├── settlement.tsx
│   └── checkoutForm.tsx
└── .env.local
```


## 実装



### Stripeのセットアップ
#### 新規登録
Stripeのアカウントを作成します。
リンクは<a href="https://dashboard.stripe.com/register">こちら</a>

![スクリーンショット (23).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060656/036655ec-b973-165f-0a90-481f7fabdcd1.png)


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

![スクリーンショット (25).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060656/3352fde1-2b54-d01a-4993-0e3595b62fab.png)

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

``` react:.env.local
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= "pk_test_***************************************"
STRIPE_SECRET_KEY="sk_test_*********************************************"
```





### API
#### Stripeのインストール
```terminal
npm install stripe
```

#### create-payment-intent.ts
```typescript: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の値は必ず指定する必要があります。詳しくは<a href="https://docs.stripe.com/api/payment_intents/create">こちら</a>を参照してください。


### フロントエンド
#### Stripeのインストール
```terminal
npm install @stripe/react-stripe-js @stripe/stripe-js
```

#### MUIのインストール
```terminal
npm install @mui/material @emotion/react @emotion/styled
```

#### settlement.tsx
```react: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
```react: 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060656/86f3bda7-d770-30a2-3278-501fa2167abf.png)

上記のように名前、値段、カード情報を入力することができます。
ダミーカードに関しては<a href="https://docs.stripe.com/testing?testing-method=card-numbers#visa">こちら</a>を参照して下さい。

![スクリーンショット (27).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060656/884c3928-44bb-4f85-6ca5-2e7613eca57d.png)
stripeの管理者ページの取引の欄を見ると先ほどの決済情報が反映されていることが分かります。



## 参考資料
https://stripe.com/jp?utm_campaign=APAC_JP_JA_Search_Brand_Payments-Pure_EXA-21278920274&utm_medium=cpc&utm_source=google&ad_content=699150004484&utm_term=stripe&utm_matchtype=e&utm_adposition=&utm_device=c&gad_source=1&gclid=Cj0KCQiApNW6BhD5ARIsACmEbkXrSs1vHrgJrvwPFAAc_ehGat44fFaTFewDz2-Lo9E7VdliaLXPuN8aAuvtEALw_wcB

https://docs.stripe.com/

https://docs.stripe.com/api



## まとめ
最後まで読んでいただきありがとうございました。
少しでも参考になれば幸いです。
また、今回の実装では決済作成のAPIしか用いなかったので値段を変えた際に前の決済処理が未完了となり新しく決済が作成されてしまいます。
![スクリーンショット (28).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3060656/9b419b25-aac4-91c3-7332-8a743afa7a64.png)

興味を持った方は<a href="https://docs.stripe.com/api/payment_intents/update">update</a>の部分の実装に挑戦してみてください！