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

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

## 前提知識

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

### Stripeのサービス

[Stripe](https://stripe.com/jp)が提供するサービスはいろいろある。

* PAYMENT（ま、普通の決済）
* BILLING（月額課金）
* CONNECT（プラットフォーマー用）

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

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

### PAYMENTの中でもいろいろ

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

#### 埋め込み方

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

<img width="635" alt="スクリーンショット 2020-01-01 10.57.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/95022a2f-1f47-1959-1876-87bdd6dcd97f.png">


#### 決済（API群）の種類

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

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

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

比較表が[本家サイト](https://www.google.com/search?q=charge+vs+payment+intent&rlz=1C5CHFA_enJP776JP776&oq=charge+vs+payment+intent&aqs=chrome..69i57j0j69i60.7232j1j9&sourceid=chrome&ie=UTF-8)にあります。


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

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

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

### React

これは私の用途限定。

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

>ReactNativeだと現時点で[tipsi-stripe](https://github.com/tipsi/tipsi-stripe)とかを利用しないと行けないみたい（ExpoをEjectせずに利用できるライブラリは無いみたいです。。。）

## paymentIntent方式のフロー

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

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

という感じ。

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

図式化したイメージ。

![スクリーンショット 2020-01-01 10.31.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/8046774d-a05f-06ae-f4b9-df555ea0fa03.png "スクリーンショット 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/9e675e7e-65fd-58c8-de50-01ee052732a1.png "スクリーンショット 2020-01-01 10.39.19.png")

>1つのクリックで上記２つの通信をします（ので分かりづらい）。

### クライアント側

ではクライアント側から。流れは[この記事](https://stripe.com/docs/recipes/elements-react)と同じですが、決済方式がchargeではなくpaymentIntetntになります。雛形作成にはcreate-react-appを利用します。


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

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

### index.html

```diff: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

```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を作成して下記のようにします。

```jsx: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を読み込みます。また、鍵の設定等も行います。

```jsx: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エラーが出るはずです。

### サーバ側

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

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

メイン実装。

```js: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を作成しています。
作成が完了したらクライアント側でに結果を戻します。

## 動作確認

### クライアント側

```bash
npm start
```

### サーバ側

```bash
node index.js
```

### Stripeダッシュボード

![スクリーンショット 2020-01-01 11.17.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/ef9fb40b-ef9c-dcba-961e-ba37a9c3f5db.png)


### その他

サーバ側をFirebase Functionsに展開してみましたが、問題なく動きました。
あと、Functionsは1回以上実行される可能性もあるので[冪等性を確保するためのkeyを付与したほうがいいという話](https://qiita.com/k-boy/items/6d8ce83084a0f49ab0d2)があります。