【Firebase】Cloud Functionsを経由してアプリ画面からStripeの商品登録を行う

# 背景

個人アプリで決済機能を導入したかったのでstripe-firebase-extensions（正式には「Run Payments with Stripe」）を使って開発を進めていたのですが、どうやら`商品をアプリ画面から登録することができない`ようなので拡張機能が用意している関数とは別にCloud Functionsを使って自分でロジックを組む必要がありました。

Stripeの管理画面から商品を登録する（Firestoreに登録する）ことはできるのでマストではなさそうですが、毎回Stripe画面へ行って商品登録するのは大変だなと思い、実装に至りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2175826/1a8f6f63-9e43-2abe-f641-1ca06ccc8276.png)

今回は図にあるようにアプリ画面からの自前のCloud Functionsを通して商品登録していきます。

※stripe-firebase-extensionsの導入から買い切り購入（Stripe決済画面への遷移）までの導線の記事も書いています。お時間ございましたら是非ご覧ください。

https://qiita.com/1zushun/items/6934f509345a0667100d

# 実装したコード（商品登録をしているメインのコード）

```javascript:controllers/stripeController.ts
import Stripe from "stripe";
import { STRIPE_API } from "../env";
import * as functions from "firebase-functions";
import { RequestHandler } from "express";

const cors = require("cors");
const stripe = new Stripe(
  STRIPE_API, { apiVersion: "2020-08-27" }
);

const stripe_post: RequestHandler = (req, res) => {
  functions.logger.info(req.body);
  cors()(req, res, async () => {
    const { photos, name, price, description, ...body } = req.body;
    const createdAt = new Date().getTime() / 1000;
    try {
      const product = await stripe.products.create({
        name: name,
        description,
        active: true, // falseにするとStripe管理画面の商品タブのアーカイブに入るのでアプリ側では表示されなくなる
        images: photos, // 2MB未満の画像でなくてはいけない
        metadata: {
          ...body,
          createdAt,
        },
      });
      await stripe.prices.create({
        unit_amount: price,
        currency: "jpy",
        product: product.id,
      });
     return res.status(200).json({ status: req.body });
    } catch (error) {
      return res.status(400).json({ status: req.body });
    }
  });
};

module.exports = {
  stripe_post,
};
```


# 今回の味噌

Stripeの管理画面から商品を手動で登録するとCloud Functions（stripe-firebase-extensions）が発火してFirestoreによしなに商品情報や金額情報格納してくれる流れです。

## ■corsの処理をindex.tsで書いているはずなのにエラーが発生する問題
関数にcorsをラップすることでエラー回避をしています。何かしらの地雷を踏んでいると思うのですが、Cloud Functionsの記事を探していると少なからず散見したので一応メモしておきます。

## ■metadataにオブジェクト型を登録することができない

Stripeが用意しているプロパティ以外の情報を登録したい場合はmetadataに入れる必要があります。

timestampを入れたかったのですが、入れられず。クライアント側でも送っても、サーバー側で`Firebase.firestore.Timestamp.toDate()`しても同様でした。

何回か別の方法で検証してみましたがプリミティブ型は大丈夫だけれど、オブジェクト型は弾かれるようです。timestampはFirebaseの仕様でオブジェクトになるのが原因っぽいですね。

## ■package-dimensionsが用意されていない

商品情報で高さや奥行きなどをまとめる`package-dimensions`というオブジェクトをStripeは用意しているのですが何回やってもエラーになるので直接ライブラリをみて確認しました。

どうやらstripe-firebase-extensionsでは用意されていない模様。フロントだけで決済機能の実装まで持っていける反面、痒いところに手が届かない感じですね。

※以下公式から引用

```javascript
const createProductRecord = async (product: Stripe.Product): Promise<void> => {
  const { firebaseRole, ...rawMetadata } = product.metadata;

  const productData: Product = {
    active: product.active,
    name: product.name,
    description: product.description,
    role: firebaseRole ?? null,
    images: product.images,
    metadata: product.metadata,
    tax_code: product.tax_code ?? null,
    ...prefixMetadata(rawMetadata),
  };
  await admin
    .firestore()
    .collection(config.productsCollectionPath)
    .doc(product.id)
    .set(productData, { merge: true });
  logs.firestoreDocCreated(config.productsCollectionPath, product.id);
};
```

https://github.com/stripe/stripe-firebase-extensions/blob/next/firestore-stripe-payments/functions/src/index.ts

# 実装したコード（呼び出し元）

ついでにExpressにして作ったのでなんちゃってMVCのコード分割はこんな感じです。

```javascript:index.ts
import * as functions from "firebase-functions";
import { Request, Response } from "express";
import * as express from "express";
import * as cors from "cors";
import {
  createJWT,
  authenticateWithJWT,
  authenticateWithFirebase,
} from "./middleware/authMiddleware";

const app = express();
const stripeRoute = require("./routes/stripeRoute");
const logActivities = require("./utilities/logger");

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors({ origin: true }));

app.post("/jwt", createJWT);

app.get(
  "/jwt/check",
  [authenticateWithJWT, authenticateWithFirebase],
  (req: Request, res: Response) => {
    res.status(200).json({ message: "認証されました。" });
  }
);

app.use("/", [authenticateWithJWT, authenticateWithFirebase], stripeRoute);

const api = functions.https.onRequest(app);
module.exports = { api, logActivities };
```

```javascript:routes/stripeRoute.ts
const express = require("express");
const router = express.Router();
const stripeController = require("../controllers/stripeController");

router.post("/stripe-post", stripeController.stripe_post);

module.exports = router;
```

# 最後に

趣味でExpressをよく触るのですが、慣れているコードで書くことができるのでさくさく進みました。

Stripeは安全性の面からクライアント側で商品登録などの操作をすることができず（と、どこかの記事で読んだはず）てっきりクライアント側からできるのではないかと思い、ここまでたどりつくのに時間がかかってしまいました。

また、あくまでも拡張機能なので実装の柔軟性を求めるのでしたらやはりサーバーをちゃんと用意して一から作った方が良いのではと思いました。しかしフロントだけで決済機能を作れるのは非常に助かります。

この記事でstripe-firebase-extensionsを使いたい方が増えましたら幸いです。

# 参考URL

https://stripe.com/docs/api