Stripe Checkout を自前のシステムに連携してスピーディーにサービスを立ち上げる

## はじめに
この記事は、[JP_Stripes Advent Calendar 2020](https://adventar.org/calendars/5626) 18日目の記事です🎉

Stripe で手軽に決済システムを作るなら `Checkout`、というのはよく知られていますが、自前のサービスとどうやって連携するか、という部分まで語られることが意外と少ない気がするので、紹介してみたいと思います😃

言語・フレームワークなどは限定されませんが、サンプルコードは PHP で記述しています。

## Stripe Checkout とは

Stripe 決済を最もクイックに実装するため Stripe が用意している仕組みで、カード入力フォーム(UI)が Stripe 決済と一体化した形で提供されます。
Stripe Elements を用いる場合と比較すると以下のような違いがあると思います。
（認識違いがあればお知らせください）
また、サンプルコード内でシークレットなどをコードにベタ書きしている箇所がありますが、各自大人の対応をよろしくお願いいたします。（環境変数化しておこう）

### Pros
- カード入力フォームの実装が不要なため、フロントの開発コストが下がる
- Customer オブジェクトの作成などが不要なため、バックエンドの開発コストも下がる

### Cons
- カード情報をユーザーと紐付けておくことができない
- デザインを自由にカスタマイズできない（ボタンの色、サービスロゴなどは設定できます）

## 想定するシステム構成・要件
- PHP, Ruby などで作られた Webシステム
- 商品情報、ユーザー情報、注文情報（決済結果を含む）は DB に保存

## 自前サービスに Checkout を連携する方法

説明に必要な箇所のみコードを紹介していますので、実際に「実装するぞ！」という場合は[ドキュメント](https://stripe.com/docs/payments/accept-a-payment)を参照してください。
（紹介するコードもほぼコピペです）

### Checkout を表示する
さてさっそくですが、 Checkout のカード入力フォームを表示してみましょう。

#### セッションを作成するための API
Checkout の表示は JavaScript から指示しますが、決済金額や支払い方法を
あらかじめ指定した「セッション」をバックエンド側で作っておきます。
正確には、「セッション」を作るためのAPIを用意します。

```php
<?php
use Slim\Http\Request;
use Slim\Http\Response;
use Stripe\Stripe;

require 'vendor/autoload.php';

$app = new \Slim\App;

$app->add(function ($request, $response, $next) {
  \Stripe\Stripe::setApiKey('sk_YOUR_SECRET_KEY');

  return $next($request, $response);
});

$app->post('/create-checkout-session', function (Request $request, Response $response) {
  $session = \Stripe\Checkout\Session::create([
    'payment_method_types' => ['card'],
    'line_items' => [[
      'price_data' => [
        'currency' => 'jpy',
        'product_data' => [
          'name' => 'Tシャツ',
        ],
        'unit_amount' => 2000,
      ],
      'quantity' => 1,
    ]],
    'mode' => 'payment',
    'success_url' => 'https://example.com/success',
    'cancel_url' => 'https://example.com/cancel',
  ]);

  return $response->withJson([ 'id' => $session->id ])->withStatus(200);
});

$app->run();
```

#### JavaScript 側からの表示

ボタンが押されたら、まずは↑で作成した API を呼び出し、得られたセッションIDを引数としてオブジェクトに入れ、Checkout の画面へとリダイレクトする `redirectToCheckout` メソッドを呼び出します。

```html
<html>
  <body>
    <button id="checkout-button">支払う</button>

    <script type="text/javascript">
      var stripe = Stripe('pk_YOUR_PUBLIC_KEY');
      var checkoutButton = document.getElementById('checkout-button');

      checkoutButton.addEventListener('click', function() {
        fetch('/create-checkout-session', {
          method: 'POST',
        })
        .then(function(response) {
          return response.json();
        })
        .then(function(session) {
          return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function(result) {
          if (result.error) {
            alert(result.error.message);
          }
        })
        .catch(function(error) {
          console.error('Error:', error);
        });
      });
    </script>
  </body>
</html>
```

リダイレクトされた Checkout の画面でユーザーが正しいカード情報を入力すれば、晴れて決済完了です🎉
ユーザーは、セッション作成時の `success_url` で指定したページにリダイレクトされます。

### Webhook で決済の結果を受け取る

さて、ここまででクレジットカードによる決済は完了しましたが、まだ自前サービスのDBにその結果が保存されていません。
保存のタイミングを考えたとき最初に思いつく方法が、「決済完了ページ（success_url に指定した画面）へのリクエストがあったら決済完了とみなす」です。
しかしこの方法には2つの問題があります。

- 悪意のあるユーザーによる不正なリクエストを防ぎきれない（実際には支払っていないのに、決済完了したと見せかけたリクエストが送られてくるかもしれない）
- ユーザーが画面から離脱してしまった場合（閉じるボタンを押した、PCが急にシャットダウンした、など）に、サービス側で決済完了を検知できない

問題が前者だけであれば、サービス側から Stripe の API を呼び出すことで「本当に決済完了したか」確認することもできますが、後者についてはこの仕組みでは対応できません。

そこで、システムにより正しく Stripe の決済結果を連携するため、 Webhook を用います。
（他にもこんな方法があるよ、などあれば教えて下さい。）

Webhook に関するドキュメントは[こちら](https://stripe.com/docs/payments/handling-payment-events)

以下のようなエンドポイントを用意しておきます。
成功時に 200 を、失敗時に 40x をステータスコードとして返すように実装しておきましょう。

```php
\Stripe\Stripe::setApiKey('sk_YOUR_SECRET_KEY');

$endpoint_secret = 'whsec_...';

$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = null;

try {
    $event = \Stripe\Webhook::constructEvent(
        $payload, $sig_header, $endpoint_secret
    );
} catch(\UnexpectedValueException $e) {
    // Invalid payload
    http_response_code(400);
    exit();
} catch(\Stripe\Exception\SignatureVerificationException $e) {
    // Invalid signature
    http_response_code(400);
    exit();
}

// Handle the event
if ($event->type === 'payment_intent.succeeded') {
    $paymentIntent = $event->data->object;
    // 支払い成功時のビジネスロジック
    handlePaymentIntentSucceeded($paymentIntent);
}

http_response_code(200);
```

上記のエンドポイントを実装したら、 Stripe のダッシュボードの Webhook 設定画面で、 `payment_intent.succeeded` のイベント時に上記エンドポイントを呼び出すように設定します。
このとき署名シークレットが発行されるので、上記の `$endpoint_secret` にセットします。
（Webhook の署名を検証して、本物の Stripe から送られたリクエスト以外を弾きます）

「ユーザーが `success_url` にリダイレクトされた時点でまだ Webhook の処理が終わっていないと困るのでは？」
と心配になるかもしれませんが、連続でエラーにならない限り、Checkout 側で Webhook の処理が終わるのを待ってくれるようです。

Webhook のエンドポイントで成功を記録し、成功画面でも記録を見て画面表示すればユーザーに対しても正しい結果を伝えることができるはずです。


## まとめ
いかがだったでしょうか？
自分は決済周りの開発をすることが多いですが、自前のシステムに決済を組み込むならこの方法が一番クイックだと思っています。
さらにスピーディーにやりたい場合は、Shopify や WooCommerce のような Stripe との連携機能が組み込まれた外部サービスの利用も検討すると良いかもしれません。

繰り返しになりますが、実装される際は [Stripeのドキュメント](https://stripe.com/docs/payments/accept-a-payment) をご確認ください。（バージョンによって仕様が変わっていることもあります）