Firebase + express + Vue.js + Payjp + TypeScriptでサーバーレスな決済処理を行う

# 話の概要

サーバーレスで決済処理をやりたいみたいな話があって、コードの部分だけ切り出してまとめました。

## ディレクトリ構造

```
payjp
├── README.md
├── babel.config.js
├── dist # firebase hostingの公開ディレクトリ
├── firebase.json
├── functions # firebase functions関連のファイル
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src #Vue.jsのソースファイル
└── tsconfig.json
```

## vuecliでVue.jsの環境構築

- vue/cliをインストールします。

```console
$ npm install -g @vue/cli
```

- 任意のプロジェクトを作成します

```console
$ vue create payjp
```

**設定は細かくは指定しませんが、ここではTypeScriptを導入します**

## Vue.jsでの画面側の作成

### vue-payjp-checkout, axiosのインストール

```console
$ cd payjp #プロジェクトディレクトリへ移動
$ npm i vue-payjp-checkout axios
```

```src/views/Home.vue
<template>
  <div>
    <payjp-checkout
      api-key="${public-key}"
      text="ポップアップを開く"
      submit-text="テストカードで支払い"
      name-placeholder="JOHN DOE"
      v-on:created="onTokenCreated"
      v-on:failed="onTokenFailed"
    ></payjp-checkout>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import axios from "axios";

@Component
export default class Home extends Vue {
  tokenId: string = "";

  onTokenCreated(token) {
    this.tokenId = token.id;

    axios
      .post(
        "http://localhost:5001/${Firebaseのプロジェクト名}/us-central1/api/charge",
        {
          token: this.tokenId
        },
        {
          withCredentials: true
        }
      )
      .then(res => {
        console.log(res);
      })
      .catch(e => {
        console.error(e);
      });
  }

  onTokenFailed(e) {
    console.error(e);
  }
}
</script>
```

```src/App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>
```

```src/main.ts
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import PayjpCheckout from 'vue-payjp-checkout'
Vue.use(PayjpCheckout)

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
```

```src/router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

  const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
```

```console
$ npm run serve
```

画面確認で

[![Image from Gyazo](https://i.gyazo.com/a76a95df16cc686588bd177d419eae26.png)](https://gyazo.com/a76a95df16cc686588bd177d419eae26)

[![Image from Gyazo](https://i.gyazo.com/d36b2797d16abb37bcbfbec07b99feb8.png)](https://gyazo.com/d36b2797d16abb37bcbfbec07b99feb8)

が出ればおk。


## Firebaseの設定

### Firebaseの構築

- firebaseでFuctions・Hosting・TypeScript、public directoryはdistを選択する

```console
$ firebase init
? Which Firebase CLI features do you want to set up for this folder?
 Press Space to select features, then Enter to confirm your choices.
 (Press <space> to select, <a> to toggle all, <i> to invert selectio
n)
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ● Functions: Configure and deploy Cloud Functions
 ● Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features

? What language would you like to use to write Cloud Functions? 
  JavaScript 
❯ TypeScript 

? Do you want to use TSLint to catch probable bugs and enforce style
? (Y/n) y

? File functions/package.json already exists. Overwrite? No
i  Skipping write of functions/package.json
? File functions/tslint.json already exists. Overwrite? No
i  Skipping write of functions/tslint.json
? File functions/tsconfig.json already exists. Overwrite? No
i  Skipping write of functions/tsconfig.json
? File functions/src/index.ts already exists. Overwrite? No
i  Skipping write of functions/src/index.ts
? File functions/.gitignore already exists. Overwrite? No
i  Skipping write of functions/.gitignore
? Do you want to install dependencies with npm now? No

? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? 
(y/N) 

? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? 
Yes
? File dist/index.html already exists. Overwrite? No
i  Skipping write of dist/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!
```

### Firebase Functionsに必要なモジュールをインストールする

```console
$ cd functions
$ npm i express payjp body-parser cors
```

### functions/src/index.tsの編集

```functions/src/index.ts

const functions = require("firebase-functions");
import { Request, Response } from "express";

const app = require("express")();
import * as Payjp from 'payjp';
const payjp = Payjp('${secret-key}');
const cors = require("cors");
const bodyParser = require("body-parser");

app.use(require("body-parser").text());
app.use(cors({ origin: true, credentials: true }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post("/charge", (req: Request, res: Response) => {
    const token = req.body.token;
    payjp.charges.create({
        amount: 1000,
        currency: "jpy",
        card: token,
        description: "test charge",
    }).then((result: Payjp.Charge) => {
        res.send(result)
    }).catch((e: Payjp.ResponseError) => {
        res.send(e)
    });
});

exports.api = functions.https.onRequest(app);
```

### 画面確認

プロジェクトのルートディレクトリに戻り、npm run buildでVue.jsのコンパイルとfirebase serveを実行しテストを行う

```console
$ cd -


$ npm run build


> form@0.1.0 build /Users/Desktop/
> vue-cli-service build


⠙  Building for production...Starting type checking service...
Using 1 worker with 2048MB memory limit
⠸  Building for production...

 DONE  Compiled successfully in 4750ms                    3:00:10 PM

  File                                 Size               Gzipped

  dist/js/chunk-vendors.e2fd753e.js    165.68 KiB         56.42 KiB
  dist/js/app.4d7b7429.js              3.25 KiB           1.63 KiB
  dist/css/app.ca936927.css            0.25 KiB           0.19 KiB

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html


$ firebase serve

=== Serving from ''...

⚠  Your requested "node" version "8" doesn't match your global version "10"
✔  functions: functions emulator started at http://localhost:5001
i  functions: Watching "" for Cloud Functions...
i  hosting: Serving hosting files from: dist
✔  hosting: Local server: http://localhost:5000
✔  functions[api]: http function initialized (http://localhost:5001/${プロジェクト名}/us-central1/api).
```

[localhost:5000](http://localhost:5000/)にアクセスし、画面を確認する。

[![Image from Gyazo](https://i.gyazo.com/a76a95df16cc686588bd177d419eae26.png)](https://gyazo.com/a76a95df16cc686588bd177d419eae26)

[![Image from Gyazo](https://i.gyazo.com/d36b2797d16abb37bcbfbec07b99feb8.png)](https://gyazo.com/d36b2797d16abb37bcbfbec07b99feb8)

[こちら](https://pay.jp/docs/testcard)からテストに必要なカードの情報を入力する。

こんな感じのログが出ればおk。

<img width="654" alt="スクリーンショット 2020-05-10 15.04.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133718/9ae55b1a-9d3e-b4bc-c3c8-7cbdcc37b316.png">

念のために決済完了したかも、[pay.jp](https://pay.jp/d/charges)で確認しましょう。

## 参考

- [Expo(ReactNative)アプリでPAY.JPを使う](https://qiita.com/mildsummer/items/026610b4f8b17b1b08c4)
- [「Vue.js + Go言語 + PAY.JP 」でクレジットカード決済できるWEBアプリケーション実装ハンズオン](https://qiita.com/po3rin/items/9638eab0a6a70faca86e)
- [Vue.js + Firebase functionsでお問い合わせフォームを作成する](https://qiita.com/ryo2132/items/7cdd6c86dd418095f74a)
- [payjp-api](https://pay.jp/docs/api/#payjp-api)
- [payjp-node](https://github.com/payjp/payjp-node)