コピペでOK！？大学に通いながらプログラミングを勉強している、貧弱エンジニアによるVue.js＋Firebase＋Stripeを使ったサブスクリプション講義（セットアップ＆実装偏）

大分タイトルが長くなりました。

皆さんこんにちは！

今や第4次産業革命とも呼ばれている「サブスクリプション」。

知らない人はいませんよね？？？

僕自身アマゾンプライム、ネットフリックス、U-NEXTのサブスクリプションに加入しております。（洋画と海外ドラマめちゃ好き。一番好きなのはプリズンブレイクです。好きな俳優はジェイソンステイサムです。）

元々、サブスクリプションというものは、企業の利益をより確実に財務表などに反映させるために作られたサービスです。これをすることで、リスクをある程度回避したうえで、株主や投資家といったお偉いさんたちにお話をできるわけです。

今やあの世界的に有名な「ポルシェ」もサブスクリプションを始めています。

まぁ、サブスクリプションの話はこの辺にしておきます。

今回はVue.js、Firebase、Stripeを使って、サブスクリプションの記事を書いていこうかなと思います。

まず初めに、@mogmetさんが書いた記事

[Firebase ExtensionsのRun Subscription Payments with Stripeを使ってサブスク課金をコードを書かずに実装する](https://qiita.com/mogmet/items/cddc182156028c928ecf)

を参考にさせて頂きました。

とても分かりやすかったです！！

まずは、こちらの記事を見て下さい。

こちらの記事を見た艇で話を進めていきます。

なぜこの記事を書こうと思ったかと言うと、Vue.jsで実行するとなると色々変えなければいけないポイントがあったからです。

記事は見たはいいものの、実装できない！

とか

これから導入していきたい！

と考えている方に丁寧に解説していくので、肩の力を抜いてご覧ください。

ただし、１つ１つの動作が何を意味をしているかはキリがないので説明致しません。

それでは一緒に説明を見ていきましょう！！！

#1, スクリプトファイルの読み込み#

``head``タグにStripeのスクリプトファイルを読み込みます。

```index.html
<script src="https://js.stripe.com/v3"></script>
```

また、特定のページだけ読み込みたいという方は、こちらの記事にやり方が書いてあるのでぜひご覧ください！

[Vue.jsでページごとにhead,meta,titleを変える方法](https://qiita.com/akki-memo/items/e79e551f4b992a9bc353)

#2, HTML要素、template要素の配置#

こちら、最初だからと言って舐めない方が良いです。

僕は、このHTML要素について十分に理解していなかったので中々時間取られました。

最初に言っておきます。

**template**要素は必ず``index.html``に書いてください！

絶対に``.vue``ファイルのtemplate内に書かないでください！！！

```index.html
<body>
    <template id="product">
      <div class="product">
        <img src="" alt="" />
        <h2>name</h2>
        <p class="description">description</p>
        <form class="product-form">
          <label for="price">Choose pricing plan</label>
          <select id="price" name="price"></select>
          <button type="submit">Subscribe</button>
        </form>
      </div>
    </template>
</body>
```

```credit.vue
<template>
<button id="signout" type="button">Sign out</button>
    <div id="subscribe">
      <h2>Subscribe</h2>
      <div class="test-card-notice">
        Use any of the
        <a href="https://stripe.com/docs/testing#cards" target="_blank" rel="noopener noreferrer">Stripe test cards </a>
        for this demo, e.g.
        <div class="card-number">4242<span></span>4242<span></span>4242<span></span>4242</div>
      </div>
      <section class="products"></section>
    </div>
    <section id="my-subscription">
      <h2>My subscription</h2>
      <p></p>
      <h3>View invoices, update subscription & payment methods</h3>
      <button id="billing-portal-button" @click="subscribeApply">Access customer portal</button>
    </section>

    <footer>
      <p>
        Made with ❤️ and ☕️ by Stripe & Firebase | View
        <a
          href="https://github.com/stripe-samples/firebase-subscription-payments"
          target="_blank"
          rel="noopener noreferrer"
          >source
        </a>
      </p>
    </footer>
</template>
```

もう一回言います。``<template id="product">``は``index.html``に書かないと後々沼にハマるのでご注意ください。

#3, dataプロパティに変数を宣言#

``stripe``と``db``と``currentUser``と言う名前の変数をdataプロパティに宣言します。

```credit.vue
<script>
export default {
  data() {
    return {
      stripe: null,
      db: null,
      currentUser: null
    }
  },
</script>
```

#4, コンテンツの表示#

次に、Stripeで追加したコンテンツ（商品）を表示していきます。

Stripeに商品を表示すると、以下のような``products``テーブルが作成されると思います。


![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954754/9df53f0f-77ee-8d78-bb49-0ffb13e82036.png)

現時点で``customers``テーブルはあっても無くても構いません。

無くても焦らずに読み進めて下さい。

``products``テーブルが作成されない場合は、設定段階でなんらかの記入ミスがあるので、見返してみてください。

そして、``startDataListeners``と言う名前の関数を``methods``内に作ります。

```credit.vue
<script>
export default {
  data() {
    return {
      stripe: null,
      db: null,
      currentUser: null
    }
  },
  methods: {
    async startDataListeners() {
      const products = document.querySelector('.products')
      const template = document.querySelector('#product')

      await this.db
        .collection('products')
        .where('active', '==', true)
        .get()
        .then((querySnapshot) => {
          querySnapshot.forEach((doc) => {
            doc.ref.collection('prices').orderBy('unit_amount').get().then((priceSnap) => {
              if (!('content' in document.createElement('template'))) {
                console.error('Your browser doesn’t support HTML template elements.')
              }
  
              const product = doc.data()
              const container = template.content.cloneNode(true)
  
              container.querySelector('h2').innerText = product.name.toUpperCase()
              container.querySelector('.description').innerText = product.description?.toUpperCase() || ''
    
              // Prices dropdown
              priceSnap.docs.forEach((doc) => {
                const priceId = doc.id
                const priceData = doc.data()
                const content = document.createTextNode(`${priceData.unit_amount}円`)
                const option = document.createElement('option')
                option.value = priceId
                option.appendChild(content)
                container.querySelector('#price').appendChild(option)
              })
  
              if (product.images.length) {
                const img = container.querySelector('img')
                img.src = product.images[0]
                img.alt = product.name
              }
  
              const form = container.querySelector('form')
              form.addEventListener('submit', this.createStripeCustomers)

              products.appendChild(container)
            })
          })
        })
      // Get all subscriptions for the customer
      this.db
        .collection('customers')
        .doc(this.currentUser)
        .collection('subscriptions')
        .where('status', 'in', ['trialing', 'active'])
        .onSnapshot(async (snapshot) => {
          if (snapshot.empty) {
            // Show products
            document.querySelector('#subscribe').style.display = 'block'
            return
          }
        })
    },
  },
}
</script>
```

ここで、``const template = document.querySelector('#product')``に注目。

先ほど口うるさく言った、「``<template id="product">``はindex.html内に書いて！」とはこのことです。

これをindex.html内に書かないと、``document.querySelector('#product')``が``null``の値を返します。

正直なぜかは分かりません。

分かる方がいたら、ぜひコメント欄にてご教えて下さると幸いです。

#5, customersテーブルの作成、およびカード情報登録画面へ遷移#

次に、customersテーブルを作成していきます。

下記のボタンを押したときの処理になります。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954754/dae4947d-5710-3cd4-7e66-316980f5fa6c.png)


``createStripeCustomers``と言う名前の関数を``methods``内に作成します。

コードが長くなるので、これまでのコードは書かないので、ご了承ください。

間違って消さないように。

```credit.vue
async createStripeCustomers(event) {
      event.preventDefault()

      document.querySelectorAll('button').forEach((b) => (b.disabled = true))
      const formData = new FormData(event.target)

      const docRef = await this.db
        .collection('customers')
        .doc(this.currentUser)
        .collection('checkout_sessions')
        .add({
          price: formData.get('price'),
          allow_promotion_codes: true,
          tax_rates: [process.env.VUE_APP_STRIPE_TAX_RATES],
          success_url: process.env.VUE_APP_BASIC_URL + '/user/account?isActive=0',
          cancel_url: process.env.VUE_APP_BASIC_URL + '/creditcardregistration',
          metadata: {
            tax_rate: '10% sales tax exclusive'
          }
        })
        // Wait for the CheckoutSession to get attached by the extension
        docRef.onSnapshot((snap) => {
        const { error, sessionId } = snap.data()
        if (error) {
          // Show an error to your customer and then inspect your function logs.
          alert(`An error occured: ${error.message}`)
          document.querySelectorAll('button').forEach((b) => (b.disabled = false))
        }
        if (sessionId) {
          // We have a session, let's redirect to Checkout
          // Init Stripe
          const stripe = window.Stripe(process.env.VUE_APP_STRIPE_PUBLIC_KEY)
          stripe.redirectToCheckout({ sessionId })
        }
        return false
      })
    },

```

ここで、注意すべき点は``tax_rates``です。

何を注意するのかと言うと、``[process.env.VUE_APP_STRIPE_TAX_RATES]``を配列として書いていることです。

なぜかは、憶測に過ぎないのですが、国によって税率が違うからだと思います。

悪魔で憶測です。鵜呑みにはしないでくださいね。

また、``process.env``なんて初めて見たという方は、こちらの記事

[初心者必見！固定値（キー、URLなど）は.envファイルに書いて再利用しよう！！](https://qiita.com/akki-memo/items/c33348cfce315548ac6b)

をご覧ください！

環境変数（process.env）は非常に便利なので、ぜひこの機会にご利用ください！！！

#6, 請求書ページに遷移#

最後に、下記のボタンを押したときの処理を書いてきます。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954754/13cbb89d-523c-152a-1043-b306d8710340.png)

当然のように、クレジットカードを利用している人が請求書を見れるようにしないといけません。

``viewInvoice``と言う名前の関数を``methods``内に作成します。

これも今までのコードは書かないのでご了承ください。

間違って消さないように。。。

```credit.vue
async viewInvoice(event) {
      this.isLoading = true
      event.preventDefault()

      document.querySelectorAll('button').forEach((b) => (b.disabled = true))

      // Call billing portal function
      const functionRef = firebase
        .app()
        .functions(process.env.VUE_APP_FUNCTION_ROCATION)
        .httpsCallable('ext-firestore-stripe-subscriptions-createPortalLink')
      functionRef({ returnUrl: window.location.href })
        .then((result) => {
          // 請求書ページへ飛ばす
          window.location.assign(result.data.url)
        })
        .catch(() => {
          alert('予期せぬエラーが発生発生したため、再度時間を置いてからご確かめください。')
        })
    },
```

``process.env.VUE_APP_FUNCTION_ROCATION``にはFirebaseで最初に設定した地域を指定します。

日本なら「asia-northeast」ですね。

これで終わりです！！

本当にコピペして頂くだけで、実行できます！

ちなみに、クレジットカードを登録しているかの確認は色々とあるのですが、下記の画像を見て下さい。

・クレジット決済を行っていない場合

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954754/b82e9f44-0ed9-aeb8-5b1a-b599da37d54b.png)

・クレジット決済を行っている場合

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954754/8fbb9988-a2b3-9003-3d83-ff2ff1cfa5a5.png)

クレジット決済を行っている場合は、``subscriptions``テーブルが作成されていますね。

このようにして判断をすることもできます。

また、こんなことをしなくても「firebase-functions」を自分で作成することで判断することもできます。

その際は、下記の記事が参考になると思うので、時間がある方はぜひご覧ください！

[さぁ、firebaseでユーザーが管理者かどうかの判別を行おう！](https://qiita.com/akki-memo/items/508c6922ec989486ab94)

##2021/02/02 追記##

サブスクリプションに加入しているかの判断を行うために、以下の記事を新しく作成したのでこちらを見た方が分かりやすいかも。

[コピペでOK！？大学に通いながらプログラミングを勉強している、貧弱エンジニアによるVue.js＋Firebase＋Stripeを使ったサブスクリプション講義（カスタムクレイム偏）](https://qiita.com/akki-memo/items/f12142cfbdbb7cf9da42)

こちらはカスタムクレイムと言って、firebaseのユーザーに独自のプロパティを追加することができる優れものなのでぜひご覧ください！

いかがだったでしょうか？

Stripeの導入は初の試みだったので、すごく大変でした。

ただ、これから学習を進めていく方々にはあまり時間を無駄にしてほしくはないので、この記事をみながら第４次産業革命の波に乗っていきましょう！！

以上、「コピペでOK！？大学に通いながらプログラミングを勉強している、貧弱エンジニアによるVue.js＋Firebase＋Stripeを使ったサブスクリプション講義」でした！

良ければ、LGTM、コメントお願いします。

また、何か間違っていることがあればご指摘頂けると幸いです。

他にも毎日初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい！！

Thank you for reading