Help us understand the problem. What is going on with this article?

(2020年元旦時点で最新の)Stripeの決済をReactで使う

どこの決済サービスを利用するかは悩ましいところですが、業界標準のStripeはいずれにしてもおさえておきたい・・・ということで調査。意外と苦労したのでメモ。

前提知識

ネットに多くの情報がありますが、仕様が変化していて最新の情報を見つけるのに苦労しました。
事前に知っていればもっと楽だったことをまとめてみます。

Stripeのサービス

Stripeが提供するサービスはいろいろある。

  • PAYMENT(ま、普通の決済)
  • BILLING(月額課金)
  • CONNECT(プラットフォーマー用)

ここの記事では PAYMENT を扱います。

他にも色々ありますが、日本では使えないものもあるので注意(Issuingとか)。

PAYMENTの中でもいろいろ

1つのサービスの中でも自サイトへの埋め込み方法やAPIの種類など複数あります。

埋め込み方

  • Checkoutを利用する(Stripeが用意した決済画面を利用する(自分のサイトに埋め込む))
  • Stripe.js/Elementを利用する(パーツとして用意されたUIとJSを利用する)

スクリーンショット 2020-01-01 10.57.47.png

決済(API群)の種類

2019年の9月にSCA Readyである必要が発生し、その対応のためにpaymentIntentが登場したもよう。
日本で言うカード情報の「非通過」、「非保持」のためのPCI-DSS対応のようなものなやつ。

これが古い記事が参考にならない原因のようです。

  • charge(古い => 事前にtokenを作るタイプのやつ(カード情報の処理が先))
  • payementIntent(新しい => 事前にpaymentIntentを作る(カード情報の処理は後))

比較表が本家サイトにあります。

client側とserver側を実装する必要がある

プログラムはクライアントとサーバ側両方での実装が必要になります(めんどい)。
技術的には1つでもいい感じがしますが、paymentIntent作成リクエストに秘密鍵が必要なので、それを隠蔽するためかなという印象。

  • server側プログラムが必要なのは 秘密鍵 を隠蔽するため(技術的にはなくても決済自体はできる)

React

これは私の用途限定。

  • Reactに特化したelementとしてreact-stripe-elementsというパッケージがある
  • 本家サイトで紹介されているのはcharge方式。ただ、paymentIntetにも対応している
  • 本記事ではreact-stripe-elementsでpaymentIntentを利用する方法を紹介

ReactNativeだと現時点でtipsi-stripeとかを利用しないと行けないみたい(ExpoをEjectせずに利用できるライブラリは無いみたいです。。。)

paymentIntent方式のフロー

では、現時点で主流のpaymentIntetを利用する決済フローを見てみます。間違ってたらご指摘を。
フローでの処理は大きく2つ。

  1. 金額を投げてpaymentIntentを作成する(紐付いたclient_secret(tokenではない)が戻る)
  2. client_securetを利用してconfirmCardPayment()を実行すると、裏でカード情報が一緒にStripeサーバに送られる

という感じ。

まず、カード情報をStripeサーバに投げて、戻ってきたtokenを利用して金額等を投げる仕様とは逆なので注意。

図式化したイメージ。

スクリーンショット 2020-01-01 10.31.08.png

実装

では上記を踏まえて実装してみます。

準備

Stripeのアカウントとかなければ作って下さい。あとはテスト用の公開キーと秘密キーがあればいいです。

  • Stripeのアカウントを作る(なければ)
  • ダッシュボードで左メニュー下段の「テストデータの表示」をOnした状態で「公開可能キー」と「シークレットキー」をメモしておく。
    • テストだとpk_test_xxxx, sk_test_xxxxという形式。本番だとtestの部分がliveになる。
  • 処理した結佐は左メニューの「支払い」から確認できる

完成図

完成予定は下記のような感じ。決済OKならアラート出します。

スクリーンショット 2020-01-01 10.39.19.png

1つのクリックで上記2つの通信をします(ので分かりづらい)。

クライアント側

ではクライアント側から。流れはこの記事と同じですが、決済方式がchargeではなくpaymentIntetntになります。雛形作成にはcreate-react-appを利用します。

必要なモジュールのインストール。

create-react-app stripe-client
cd stripe-client
npm install --save react-stripe-elements bootstrap reactstrap formik yup

index.html

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="description" content="Payment for e-money service" />
+  <script src="https://js.stripe.com/v3/"></script>
  <title>Payment Site</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

index.js

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

実装。App.jsと同じ階層にCheckoutForm.jsを作成して下記のようにします。

CheckoutForm.js
import React from 'react';
import { CardElement, injectStripe, CardNumberElement, CardExpiryElement, CardCVCElement, Elements } from 'react-stripe-elements';
import { Button, Form, FormGroup, Label, Input, FormFeedback } from 'reactstrap';
import { Formik } from 'formik'
import * as Yup from 'yup';

class CheckoutForm extends React.Component {

    handlePayment = async (values) => {

        // alert(JSON.stringify(values));

        const headers = new Headers();
        headers.set('Content-type', 'application/json');
        // headers.set('Access-Control-Allow-Origin', '*');

        //paymentIntentの作成を(ローカルサーバ経由で)リクエスト
        const createRes = await fetch('http://localhost:9000/createPaymentIntent', {
            method: 'POST',
            headers: headers,
            body: JSON.stringify({ amount: values.amount, username: values.username })
        })

        //レスポンスからclient_secretを取得
        const responseJson = await createRes.json();
        const client_secret = responseJson.client_secret;

        //client_secretを利用して(確認情報をStripeに投げて)決済を完了させる
        const confirmRes = await this.props.stripe.confirmCardPayment(client_secret, {
            payment_method: {
                // card: this.props.elements.getElement('card'),
                card: this.props.elements.getElement('cardNumber'),
                billing_details: {
                    name: values.username,
                }
            }
        });

        if (confirmRes.paymentIntent.status === "succeeded") {
            alert("決済完了");
        }
    }

    render() {
        console.log(this.props.stripe);
        return (
            <div className="col-8">
                <p>決済情報の入力</p>
                <Formik
                    initialValues={{ amount: 100, username: 'TARO YAMADA' }}
                    onSubmit={(values) => this.handlePayment(values)}
                    validationSchema={Yup.object().shape({
                        amount: Yup.number().min(1).max(1000),
                    })}
                >
                    {
                        ({ handleChange, handleSubmit, handleBlur, values, errors, touched }) => (
                            <Form onSubmit={handleSubmit}>
                                <FormGroup>
                                    <Label>金額</Label>
                                    <Input
                                        type="text"
                                        name="amount"
                                        value={values.amount}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.amount && errors.amount)}
                                    />
                                    <FormFeedback>
                                        {errors.amount}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <Label>利用者名</Label>
                                    <Input
                                        type="text"
                                        name="username"
                                        value={values.username}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.username && errors.username)}
                                    />
                                    <FormFeedback>
                                        {errors.username}
                                    </FormFeedback>
                                </FormGroup>
                                {/* <CardElement
                                    className="bg-light p-3"
                                    hidePostalCode={true}
                                /> */}
                                <legend className="col-form-label">カード番号</legend>
                                <CardNumberElement
                                    ref={this.cardNumberRef}
                                    className="p-2 bg-light"
                                />
                                <legend className="col-form-label">有効期限</legend>
                                <CardExpiryElement
                                    className="p-2 bg-light"
                                />
                                <legend className="col-form-label">セキュリティーコード</legend>
                                <CardCVCElement
                                    className="p-2 bg-light"
                                />

                                <Button
                                    onClick={this.submit}
                                    className="my-3"
                                    color="primary"
                                >
                                    購入
                                </Button>
                            </Form>
                        )
                    }
                </Formik>

            </div>
        );
    }
}

export default injectStripe(CheckoutForm);

App.jsでCheckoutForm.jsを読み込みます。また、鍵の設定等も行います。

App.js
import React from 'react';
import { Elements, StripeProvider } from 'react-stripe-elements';
import CheckoutForm from './CheckoutForm';

function App() {
  return (
    <StripeProvider apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
      <div className="container">
        <h3 className="my-4">React Stripe Element Sample</h3>
        <Elements>
          <CheckoutForm />
        </Elements>
      </div>
    </StripeProvider>
  );
}

export default App;

これでクライアント側は一旦完了。ボタンを押すと404エラーが出るはずです。

サーバ側

続いてサーバ側。
まず、必要なモジュールをインストールします。

mkdir stripe-server
cd stripe-server
npm init -f
npm install express body-parser stripe

メイン実装。

index.js
const app = require("express")();
const stripe = require("stripe")("sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
const cors = require('cors');
const bodyParser = require('body-parser');

app.use(require("body-parser").text());
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post('/createPaymentIntent', async (req, res) => {

    const result = await stripe.paymentIntents.create({
        amount: req.body.amount,
        currency: 'jpy',
        description: '●●商店決済', //option
        metadata: { username: req.body.username, tranId: '11111' } //option
    });

    console.log(result);
    res.json(result);

});

app.listen(9000, () => console.log("Listening on port 9000"));

stripe.paymentIntetns.create()が裏でStripeサーバと通信をしてIntentを作成しています。
作成が完了したらクライアント側でに結果を戻します。

動作確認

クライアント側

npm start

サーバ側

node index.js

Stripeダッシュボード

スクリーンショット 2020-01-01 11.17.56.png

その他

サーバ側をFirebase Functionsに展開してみましたが、問題なく動きました。
あと、Functionsは1回以上実行される可能性もあるので冪等性を確保するためのkeyを付与したほうがいいという話があります。

zaburo
こんにちは。自分用のメモをだらだら公開しています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした