Nuxt3でStripeを利用したサブスクリプション申し込みフォームを作成する

この記事は、[Vue Advent Calendar 2022](https://qiita.com/advent-calendar/2022/vue) 12日目の記事です。

Stripeを利用してサブスクリプションやオンライン決済を処理するには、フロントエンドアプリだけでなくサーバーサイドの処理も必要です。

Vueの場合、Nuxtを利用することで、フロントエンド・サーバーサイドのアプリを１つにまとめて構築・運用できます。

今回は、2022年11月にローンチされたばかりの、Nuxt3を利用してサブスクリプションの申し込みフォームを用意する方法を紹介します。

https://nuxt.com/v3

## ViteでNuxt3プロジェクトをセットアップする

今回プロジェクトのセットアップには、Viteを利用しました。

```bash
$ yarn create vite
```

プロジェクト名を指定した後、「利用するフレームワーク」と「アプリの設定（JS / TS / create-vueをカスタマイズ / nuxt）」を選べますので、`Vue`と`Nuxt`を選びましょう。

```bash
✔ Project name: … vite-nuxt
✔ Select a framework: › Vue
✔ Select a variant: › Nuxt ↗
```

Nuxtがインストールされます。

```bash
Need to install the following packages:
  nuxi@3.0.0
Ok to proceed? (y) y
Nuxi 3.0.0                                                                                                                                      10:12:00
✨ Nuxt project is created with v3 template. Next steps:                                                                                        10:12:01
 › cd vite-nuxt                                                                                                                                 10:12:01
 › Install dependencies with npm install or yarn install or pnpm install                                                                        10:12:01
 › Start development server with npm run dev or yarn dev or pnpm run dev                                                                        10:12:01
✨  Done in 13.99s.
```

最後に、作成したプロジェクトディレクトリへ移動して、ライブラリをインストールします。

```bash
$ cd vite-nuxt
$ yarn
$ yarn dev
```

起動に成功すれば、ローカルで確認するURLが表示されますので、アクセスしましょう。

```bash
[get-port] Unable to find an available port (tried 3000). Using alternative port: 3001

  > Local:    http://localhost:3001/ 
  > Network:  http://192.123.45.46:3001/

✔ Nitro built in 251 ms
ℹ Vite client warmed up in 747ms
```

デフォルトの表示が確認できれば、セットアップ完了です。

![スクリーンショット 2022-11-29 11.09.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/ae3a93f3-559d-9f5d-b8d2-b6f485ec64c2.png)

## Stripeのサブスクリプション申し込みフローを、Nuxt3で組み込む

ここからは、Nuxt3アプリにサブスクリプション申し込みフォームを組み込みます。

Stripeでの組み込みは、次の３ステップで行えます。

+ [Server] Stripe上に顧客・サブスクリプションデータを作成する
+ [Client] 作成されたサブスクリプションデータのclient secretで、決済フォームを表示する
+ [Client] フォームのsubmit処理で、サブスクリプションの申し込みを完了する

### 事前準備: ライブラリインストールと環境変数でのAPIキー設定

StripeのSDKを利用して組み込みますので、ライブラリをインストールしましょう。

```bash
$ yarn add stripe @stripe/stripe-js
```

また、StripeのAPIキーを.env.localファイルに保存しましょう。

```bash
VITE_STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
```

### [Server] Stripe上に顧客・サブスクリプションデータを作成する

まずサブスクリプションデータを作成するAPIを用意しましょう。

`server/api/subscription.post.ts`ファイルを作成し、以下のコードを追加しましょう。

```ts
import { Stripe } from 'stripe'

export default async function () {
    const config = useRuntimeConfig()
    const stripe = new Stripe(config.stripeSecretAPIKey, {
        apiVersion: '2022-11-15'
    })
    const customer = await stripe.customers.create()
    const product = await stripe.products.create({
        name: 'Nuxt3デモ用'
    })
    const subscription = await stripe.subscriptions.create({
        customer: customer.id,
        items: [{
            price_data: {
                unit_amount: 1000,
                currency: 'jpy',
                recurring: {
                    interval: 'month',
                },
                product: product.id
            }
        }],
        payment_behavior: 'default_incomplete',
        expand: ['latest_invoice.payment_intent'],
        payment_settings: {
            save_default_payment_method: 'on_subscription',
        },
    })

    return {
        subscriptionId: subscription.id,
        clientSecret: ((subscription.latest_invoice as Stripe.Invoice).payment_intent as Stripe.PaymentIntent).client_secret
    }
}
```

#### Subscription作成処理の参考記事について

今回のサンプルコードは、事前にダッシュボードでデータを作成する必要のないように用意しました。

事前準備が不要な代わりに**ページ読み込みの度にCustomerや商品データが生成**されますのでご注意ください。

引数の解説等については、以下の記事で紹介しています。

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

https://qiita.com/hideokamoto/items/6bd31422e2ebb27d4eaa

### [Client] 作成されたサブスクリプションデータのclient secretで、決済フォームを表示する

APIを作成しましたので、フロントエンドで呼び出してStripe Elementsの決済フォームを表示させましょう。

```vue
<template>
   <NuxtLayout>
    <form>
      <div id="payment-element"></div>
      <button type="submit">Subscribe</button>
    </form>
  </NuxtLayout>
</template>

<script setup lang='ts'>
import { loadStripe } from '@stripe/stripe-js'
const runtimeConfig = useRuntimeConfig()
const { stripePublicAPIKey } = runtimeConfig.public

let elements
let paymentElement
let stripe

const { clientSecret } = await $fetch('/api/subscription', {
  method: 'POST'
})

if (!process.server) {
  stripe = await loadStripe(stripePublicAPIKey)
  elements = stripe.elements({
    clientSecret,
    appearance: {
      theme: 'stripe'
    }
  })
  paymentElement = elements.create('payment')
  paymentElement.mount('#payment-element')
}

</script>
```

作成したAPIでサブスクリプションのclient secretを作成し、フロントエンド側の処理でElementによる決済フォームの表示処理を行なっています。

ページを再読み込みして、決済フォームが表示されればOKです。

![スクリーンショット 2022-11-29 12.28.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/870c946d-5a06-aeee-904c-6cb5a4aefc12.png)


### [Client] フォームのsubmit処理で、サブスクリプションの申し込みを完了する

最後にFormにSubmitイベントを設定して、サブスクリプション申し込みを完了する処理を書きましょう。

`form`タグにsubmitイベントを設定します。

```diff_vue
<template>
   <NuxtLayout>
-    <form>
+    <form @submit.prevent="confirmSubscription">
      <div id="payment-element"></div>
      <button type="submit">Subscribe</button>
    </form>
  </NuxtLayout>
</template>
```

`confirmSubscription`を実装して、処理を完了させます。

```ts

async function confirmSubscription() {
  const { error } = await stripe.confirmPayment({
    elements,
    redirect: 'if_required'
  })
  if (error) {
    window.alert(JSON.stringify(error))
  } else {
    window.alert('done')
  }
}
```

テストのクレジットカード番号を入力して、[done]のメッセージが表示されていれば、成功です。

https://stripe.com/docs/testing

![スクリーンショット 2022-11-29 12.29.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/3e18a846-83ee-a2f5-2a4f-5bf1073ba726.png)

Stripeダッシュボードでも、サブスクリプションの申し込みが完了していることが確認できます。

![スクリーンショット 2022-11-29 12.30.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/5eb4ed1a-7c28-b632-45f8-694ad1422929.png)