StripeをReactで作られたサービスに組み込んでみた話
2017-12-02

StripeをReactで作られたサービスに組み込んでみた話

この記事はStripe Advent Calendar 2017 - Adventar2日目の記事です。

自分がフロントエンドをやっている、オンラインプログラミング学習サービスCODEPREPに、Stripe決済を組み込んでみた時の話です。

(注意)CODEPREPは2018年1月4日をもってプレミアム会員プランを停止したため、このページはもう見ることはできません。

はじめに

CODEPREPはReactで作られているので、こちらのReactコンポーネントを使ってみました。

自分がやったのは2017年07月くらいなので、一部内容が最新ではないものがあります。ご注意ください。

仕上がりはこんな感じです。
スタイル周りのカスタマイズが結構できたので、サービスに溶け込ませるように組み込むことができました。

導入方法

紹介するコード概念を説明するものなので、そのままでは動かないかもしれません。ご注意ください。

導入方法はnpm install --save react-stripe-elementsしてから、Reactコンポーネントを使っていきます。

StripeProviderでアプリケーションをラップする

まず最初にStripeProviderでアプリケーションのルートコンポーネントをラップします。
この時にapiKeyにアプリケーションアクセスキーを設定してください。

// index.jsx
import React from 'react';
import { render } from 'react-dom';
import { StripeProvider } from 'react-stripe-elements';

import Main from './Main.jsx'

const App = () => {
  return (
    <StripeProvider apiKey="<YOUR_APP_KEY>">
      <Main />
    </StripeProvider>
  );
};

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

Elementsで支払いフォームをラップする

次にElementsで支払いフォームをラップします。Elementsは、Stripeの入力フォームをグルーピングするために利用するコンポーネントのようです。

// myCheckout.jsx
import React from 'react';
import { Elements } from 'react-stripe-elements';

import CheckoutForm from './CheckoutForm.jsx'

class MyCheckout extends React.Component {
  render() {
    return (
      <Elements>
        <CheckoutForm />
      </Elements>
    );
  }
}

export default MyCheckout;

injectStripeで支払いフォームをラップする

さらにinjectStripeで支払いフォームをラップします。

injectStripeは、Stripeの入力フォームの様々な入力イベントを処理するために利用するHigher-Order Component(HOC)です。

公式ドキュメントでは、injectStripeElements一緒に使えないことになっているので、何も考えずElementsinjectStripeを使うコンポーネントを2つに分けた方がいいです。

// checkoutForm.jsx
import React from 'react';
import { injectStripe } from 'react-stripe-elements';

class CheckoutForm extends React.Component {
  render() {
    return (
      <form>
        ここに入力フォームが入る
      </form>
    );
  }
}

export default injectStripe(CheckoutForm);

カード情報入力フォームを配置する

最後にカード情報入力用のコンポーネントを配置していきます。

StripeのReactコンポーネントには、All-on-one型のCardElementと、これらを別々に分割したCardNumberElement, CardExpiryElement, CardCVCElementがあります。

自分の場合は、レイアウトを自由にしたかったので、別々に分割されたコンポーネントを利用しました。

かなりラフですが。。。入力部品を配置したらこのような感じになると思います。

// checkoutForm.jsx
import React from 'react';
import { injectStripe } from 'react-stripe-elements';

class CheckoutForm extends React.Component {
  render() {
    return (
      <form>
        <CardNumberElement />
        <CardExpiryElement />
        <CardCVCElement />
        <button>支払い</button>
      </form>
    );
  }
}

export default injectStripe(CheckoutForm);

これで導入については終わりです。次からは細かなイベントハンドリング方法やカスタマイズについて紹介します。

入力フォームのイベントハンドリング

入力フォームのイベントハンドリング方法について紹介します。

フォームの入力チェック

フォームの入力チェックはStripeの入力コンポーネントのonChangeにハンドラを設定して処理します。

// checkoutForm.jsx

...

class CheckoutForm extends React.Component {
+  constructor(props) {
+    super(props);
+    this.handleChange = this.handleChange.bind(this);
+  }

  render() {
    return (
      <form>
-        <CardNumberElement />
+        <CardNumberElement onChange={this.handleChange}/>
        <CardExpiryElement />
        <CardCVCElement />
        <button>支払い</button>
      </form>
    );
  }

+  handleChange(data) {
+    if (!data.complete) {
+      // ERROR
+      console.log(data.error.message);
+    }
+  }
}

onChangeハンドラには次のようなStripeのデータオブジェクトが渡されてくるので、これを元にエラーかどうか判断します。

{
  brand: "amex",
  complete: false,
  empty: false,
  error: {
    code: "invalid_number",
    message: "カード番号が無効です。",
    type: "validation_error"
  },
  value: undefined
}

エラーかどうかはcompleteを、メッセージはerror.massageを見ればいいと思います。

注意点としては、このオブジェクトは入力フィールド単体で送られてくるので、フォーム全体を管理するためには別途Stateなどを使って状態を管理する必要があるということです。
ちょっと泥臭い実装ですが、自分はそれぞれの入力フォームのハンドラを別に定義して処理しました。

フォームの送信処理

フォームの送信処理は、フォームのonSubmitにハンドラを設定して処理します。

現在のところフォーム送信処理はPaymentRequestButtonElementを使った方がエレガントかもしれません。

Stripeへの支払い処理にはstripe.createTokenを呼び出して、カード情報と支払い情報をStripe上に登録する必要があります。処理が完了すると、Stripeはトークンを発行するのでこれをバックエンドのデータベースなどに保存しておきます。

stripe.createTokeninjectStripeで設定されるオブジェクトです。injectStripeでラップされたコンポーネントはpropsからこのオブジェクトを利用することが可能です。

// checkoutForm.jsx

...

class CheckoutForm extends React.Component {
  constructor(props) {
    super(props);
+    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  render() {
    return (
-      <form>
+      <form onSubmit={this.handleSubmit}>
        <CardNumberElement onChange={this.handleChange}/>
        <CardExpiryElement />
        <CardCVCElement />
        <button>支払い</button>
      </form>
    );
  }

  handleChange(data) {
    if (!data.complete) {
      // ERROR
      console.log(data.error.message);
    }
  }

+  handleSubmit(e) {
+    // ここにバリデーション処理
+   
+    this.props.stripe.createToken().then(result => {
+      if (result.error) {
+        // ERROR
+        console.log(result.error.message);
+        return;
+      }
+      // ここで取得したトークンをバックエンドに送信して完了!!
+      console.log(result.token);
+    });
+  }
}

スタイルのカスタマイズ方法

Stripeのコンポーネントはスタイルをカスタマイズすることが可能です。これを行うことで、サービスに溶け込ませるようにStripeを組み込むことができます。

自分の場合は、バリデーションエラーのスタイルをカスタマイズする必要がありました。なので、この手順はほぼ必須かと思います。
最初やり方がわからなかったのでStack Overflowで聞いてみました。

実際にrenderされた後のコードを見るとわかるのですが、Stripeのコンポーネントは.StripeElementというスタイルを持つDOMにラップされています。

.StripeElementはCSS命名規則のBEMに則ったいくつかの状態(modifier)を持つので、これらのスタイルを変更することでカスタマイズか可能です。

.StripeElement {
  border: 1px solid #eee;
}

.StripeElement--invalid {
  border: 1px solid red;
}

.StripeElement--focus {
  border: 1px solid blue;
}

こんな感じでスタイルをカスタマイズできます。

現在は、ElementsstyleにCSSスタイルをセットするとスタイルが適用されるようです。便利。

まとめ

自分がStripeを導入した時の手順でした。
2017年7月にやったのですが、既にいくつかやり方が古くなってるものがありますね。

アップデートが早いようなので、実際に組み込む前に公式のドキュメントを確認した方が良さそうです。

おまけ

導入当時はJCBに対応してなかったので、Stripeが対応してくれた時はチーム全員で大喜びしました。懐かしい記憶です。

(Slackのログを確認したら、2017-08-22の出来事のようですね)