Honoを利用したウェブアプリケーションに、Stripeの決済フォーム（Elements / Embedded Checkout）を実装する方法

この記事は、「[Hono Advent Calendar 2023](https://qiita.com/advent-calendar/2023/hono)」と「[JP_Stripes Advent Calendar 2023](https://adventar.org/calendars/9070)」6日目の記事です。

HonoはCloudflare　Workers / Pagesだけでなく、VercelやAWS Lambda・DenoそしてNode.jsを実行するDockerコンテナまで、幅広い実行環境で利用できるJavaScript / TypeScriptのフレームワークです。Honoを利用すると、デプロイしたい環境に合わせた設定を少し追加するだけで、複数の環境にデプロイできるWeb Application / APIが作れます。

https://hono.dev/getting-started/nodejs

https://hono.dev/getting-started/cloudflare-workers

https://hono.dev/getting-started/deno

## HonoでStripeの決済フォームを実装する

Honoには、REST APIだけでなくHTMLなども配信する機能が存在します。サーバー側でStripe APIなどを呼び出した後、その結果を利用してアプリケーションのフロントエンドを配信するなどの実装もHonoだけで行うことができます。

https://hono.dev/helpers/html

そこで今回はHonoとStripe SDK (Node / JavaScript)を利用して、オンライン決済フォームを構築する方法を紹介します。

### Honoでアプリをセットアップする

まずはアプリケーションをセットアップしましょう。Honoの場合、`create-hono`コマンドを利用します。

```bash
npm create hono@latest my-app
```

どの実行環境・サービスにデプロイするかを聞かれます。今回はローカル環境でのみ試しますので、デフォルト設定の`cloudflare-workers`を選びましょう。

```bash
? Which template do you want to use?
    aws-lambda
    bun
    cloudflare-pages
❯   cloudflare-workers
    deno
    fastly
    lagon
    nextjs
    nodejs
    vercel
```

アプリを作成後、StripeのSDKをインストールします。`stripe`はサーバー側で利用するSDKです。

```bash
npm i stripe
```

APIキーについては、フロントエンド用の公開可能キー（`STRIPE_PUBLISHABLE_KEY`）とシークレットキー（`STRIPE_SECRET_KEY`）の2つを設定します。Cloudflare Workersをローカルで動かす場合、`.dev.vars`ファイルを作成して、その中に保存しましょう。

```.dev.vars
STRIPE_PUBLISHABLE_KEY='pk_test_xxxxxxx'
STRIPE_SECRET_KEY='sk_test_xxxxxxx'
```

### Payment Intentsを利用して、決済フォームを表示する

Honoでは、サーバー側の処理を実行した上で、HTMLを作成することができます。そのため、「Payment Intentをサーバー側で作成する」ことと「作成されたPayment Intentの情報を使って、決済フォームを表示する」ことを１ファイルで記述できます。

次のコードでは、19.8万円の決済フォームを表示する処理を、TypeScriptで実装しています。

```ts
import { Hono } from "hono";
import Stripe from "stripe";

type Bindings ={
    STRIPE_PUBLISHABLE_KEY: string;
    STRIPE_SECRET_KEY: string;
};
const app = new Hono<{
    Bindings: Bindings
}>();


app.get("/", async c => {
    const stripe = new Stripe(c.env.STRIPE_SECRET_KEY);
    const paymentIntent = await stripe.paymentIntents.create({
        amount: 198000,
        currency: "jpy",
    });
    return c.html(`
<html>
    <head>
        <title>Elements</title>
        <style>
            html,
            body {
                height: 100%;
            }

            main {
                display: flex;
                align-items: center;
                justify-content: center;
                flex-direction: column;
                padding-top: 40px;
                padding-bottom: 40px;
                background-color: #fefefe;
            }
            form > * {
                margin-bottom: 20px;
            }
        </style>
        <script src="https://js.stripe.com/v3/"></script>
    </head>
    <body>
        <main>
            <h2>Order</h2>
            <form id="order-form">
                <div id="payment-element"></div>
                <button type="submit">Order</button>
            </form>
        </main>
        <script>
            const stripe = Stripe("${c.env.STRIPE_PUBLISHABLE_KEY}");
            const elements = stripe.elements({
                clientSecret: "${paymentIntent.client_secret}",
                appearance: {
                    theme: "stripe"
                }
            });
            const paymentElement = elements.create("payment");
            paymentElement.mount("#payment-element");
            const form = document.getElementById("order-form");
            form.addEventListener("submit", async e => {
                e.preventDefault();
                const result = await stripe.confirmPayment({
                    elements,
                    redirect: "if_required",
                });
                console.log(result);
                alert("Order Complete");
            });
        </script>
    </body>
</html>    
    `)
})

export default app
```

#### 決済フォームを表示する処理を、段階ごとに解説

コードを少し分解してみてみましょう。まず次のコードは、アプリに設定した環境変数をTypeScriptで取り扱うためのBinding設定を行っています。ここでは２つのStripe APIキーを追加しました。

```ts
type Bindings ={
    STRIPE_PUBLISHABLE_KEY: string;
    STRIPE_SECRET_KEY: string;
};
const app = new Hono<{
    Bindings: Bindings
}>();
```

Honoで実装しているAPIは、`GET /`の１つだけです。`c.html`を利用して、HTMLファイルを返すことで、このURLにアクセスした人にWebページを表示させることができます。

```ts
app.get("/", async c => {
    return c.html(`
<html>
    <head>
        <title>Elements</title>
    </head>
    <body>
        <main>
            <h2>Order</h2>
            <form id="order-form">
                <div id="payment-element"></div>
                <button type="submit">Order</button>
            </form>
        </main>
    </body>
</html>    
    `);
});

export default app;
```

HTMLを返す大枠ができましたので、Stripe SDKを利用して決済に利用するPayment Intentを作成します。今回はシンプルに金額と通貨だけを指定しました。

```diff_typescript
app.get('/', async c => {
+    const stripe = new Stripe(c.env.STRIPE_SECRET_KEY)
+    const paymentIntent = await stripe.paymentIntents.create({
+        amount: 198000,
+        currency: 'jpy',
+    })
    return c.html(`
<html>
    <head>
        <title>Elements</title>
    </head>
    <body>
        <main>
            <h2>Order</h2>
            <form id="order-form">
                <div id="payment-element"></div>
                <button type="submit">Order</button>
            </form>
        </main>
    </body>
</html>    
    `);
});

export default app;
```

:::note
オーソリとキャプチャを分離したいケースや、保存された支払い情報を利用したいケースなどでは、APIドキュメントを参考にリクエストパラメータをカスタマイズしましょう。

- https://stripe.com/docs/api/payment_intents/create
- https://stripe.com/docs/payments/place-a-hold-on-a-payment-method
- https://stripe.com/docs/payments/save-during-payment

:::

作成したPayment Intentを利用して、決済フォームを表示する処理を追加します。この処理はクライアント側で実行する必要がありますので、`script`タグの中に「クライアント側（ブラウザ上）で実行するJavaScript」として実装します。

```diff_typescript
        <main>
            <h2>Order</h2>
            <form id="order-form">
                <div id="payment-element"></div>
                <button type="submit">Order</button>
            </form>
        </main>
+        <script>
+            const stripe = Stripe("${c.env.STRIPE_PUBLISHABLE_KEY}");
+            const elements = stripe.elements({
+                clientSecret: "${paymentIntent.client_secret}",
+                appearance: {
+                    theme: "stripe"
+                }
+            });
+            const paymentElement = elements.create("payment");
+            paymentElement.mount("#payment-element");
+        </script>
    </body>
```

最後に`form`タグの`submit`イベントを利用して、決済処理を完了するコードを実装しましょう。`redirect: "if_required"`を設定することで、3Dセキュア認証やクレジットカード以外の一部の決済手段など、支払いを完了するためにリダイレクトが必要な決済手段の時のみリダイレクトを行うように設定しています。

```diff_typescript
            const paymentElement = elements.create("payment");
            paymentElement.mount("#payment-element");
+            const form = document.getElementById("order-form");
+            form.addEventListener("submit", async e => {
+                e.preventDefault();
+                const result = await stripe.confirmPayment({
+                    elements,
+                    redirect: "if_required",
+                });
+                console.log(result);
+                alert("Order Complete");
+            });
        </script>
    </body>
```

#### 実際のアプリケーションに組み込む場合

シンプルな決済フォームの実装サンプルを紹介しました。しかしこれでは特定の金額のみしか注文できません。実際のECアプリケーションなどに組み込む場合は、CookieまたはCloudflare　Workers KVなどのKey-Valueストアを利用して、「カートの中身」をユーザー・購入セッションごとに管理する仕組みを追加しましょう。

https://www.cloudflare.com/ja-jp/developer-platform/workers-kv/

## HonoでStripe Checkoutのフォームを埋め込むコードサンプル

よりシンプルに高機能な決済フォームを組み込みたい場合には、Stripe Checkoutを埋め込む方法も利用できます。

埋め込み型のStripe Checkoutを利用する場合は、次のようなAPIを実装しましょう。

```typescript

app.get('/checkout', async c => {
    const stripe = new Stripe(c.env.STRIPE_SECRET_KEY)
    const session = await stripe.checkout.sessions.create({
      line_items: [{
        price: 'price_から始まる料金ID',
        quantity: 1,
      }],
      mode: 'payment',
      ui_mode: 'embedded',
      return_url: 'https://example.com/checkout/return?session_id={CHECKOUT_SESSION_ID}'
    });

    return c.html(`
<html>
    <head>
        <title>Elements</title>
        <style>
            html,
            body {
                height: 100%;
            }

            main {
                display: flex;
                align-items: center;
                justify-content: center;
                flex-direction: column;
                padding-top: 40px;
                padding-bottom: 40px;
                background-color: #fefefe;
            }
            form > * {
                margin-bottom: 20px;
            }
            #payment-element {
                width: 80%;
            }
        </style>
        <script src="https://js.stripe.com/v3/"></script>
    </head>
    <body>
        <header>
            <h1>Hello</h1>
        </header>
        <main>
            <div id="payment-element"></div>
        </main>
        <script>
            const stripe = Stripe("${c.env.STRIPE_PUBLISHABLE_KEY}")
            stripe.initEmbeddedCheckout({
                clientSecret: "${session.client_secret}",
            }).then(session => {
                checkout.mount("#payment-element")
            });
        </script>
    </body>
</html>    
    `)
})
```

## おわりに

Stripeの提供する決済フォームや注文（Checkout）フォームは、Honoを利用することでとてもシンプルに組み込むことができます。もちろんPayment IntentやCheckout Sessionを作成するAPIを別途用意して、クライアント側の`fetch`で対応するなどの組み込みも可能でし、Cloudflare PagesやNext.jsなどと組み合わせた場合には、よりリッチな組み込みもできます。

また、我々（Stripe）が提供しているサンプルアプリとして、Cloudflare Workers向けのものも公開しています。

https://github.com/stripe-samples/stripe-node-cloudflare-worker-template

こちらのサンプルも、2023年にHonoを利用した実装に変更しましたので、ぜひ参考にしてください。

https://github.com/stripe-samples/stripe-node-cloudflare-worker-template/pull/12/files

その他にも、Stripe WebhookをCloudflare Workersで処理する方法や、Turnstileを組み合わせる方法などもQiitaに公開しています。

https://qiita.com/hideokamoto/items/7517f14f801152063926

https://qiita.com/hideokamoto/items/c53649599ff993a0747c

2024年も、皆様とHono / CloudflareとStripeの組み合わせ方や活用事例を模索していくことができればと思います。

来年もどうぞよろしくお願いします。