はじめに
今回、watnowアドベントカレンダー8日目を担当させていただきます。
この記事では、StripeのAPIを用いたシンプルな決済処理を実装していきます。
使用するツールや言語
- Next.js
- MUI
- Stripe
実装するもの
名前、値段選択ができるフォームからStripeを用いて決済処理を行う。
フォルダ構造
.
├── pages/
│ ├── api/
│ │ └── create-payment-intent.ts
│ ├── settlement.tsx
│ └── checkoutForm.tsx
└── .env.local
実装
Stripeのセットアップ
新規登録
Stripeのアカウントを作成します。
リンクはこちら
keyの取得
アカウント設定が終わると、新しいビジネスとして下のような管理者ページに遷移すると思います。今回はこのテスト環境で実装していきます。
上記の画像の右にある公開可能キー、シークレットキーをコピーしてenvファイルに保存しておいてください。
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= "pk_test_***************************************"
STRIPE_SECRET_KEY="sk_test_*********************************************"
API
Stripeのインストール
npm install stripe
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
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
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
: カード情報入力フォームを作成します。
実行結果
上記のように名前、値段、カード情報を入力することができます。
ダミーカードに関してはこちらを参照して下さい。
stripeの管理者ページの取引の欄を見ると先ほどの決済情報が反映されていることが分かります。
参考資料
まとめ
最後まで読んでいただきありがとうございました。
少しでも参考になれば幸いです。
また、今回の実装では決済作成のAPIしか用いなかったので値段を変えた際に前の決済処理が未完了となり新しく決済が作成されてしまいます。
興味を持った方はupdateの部分の実装に挑戦してみてください!