FirebaseにStripe Checkoutを組み込む【備忘録】

# 今回の構成
* expressを使わない
* クライアントはvue3を使用
* functionsはtypescriptで記述
* firebase拡張機能のRun Payments with Stripeを使わない

# 手順
## サブスクリプションの実装
はじめに、funtionsフォルダへ移動してstripeをインストール
```bash:functions/
$ npm install --save stripe
```

firebaseで環境変数を構成
```bash:functions/
$ firebase functions:config:set stripe.secret=sk_test_51JK...
```

登録されているか確認する。

```bash:functions/
$ firebase functions:config:get
```
以下のような形で表示されていればOK
```json
{
  "stripe": {
    "secret": "sk_test_51JK..."
  }
}
```
### サーバー側のコードを書いていく

` {{PRICE_ID}} `にはStripeダッシュボードで事前に作成した商品のIDを記述する。
```typescript:server.ts
import * as functions from "firebase-functions";
import { RuntimeOptions } from "firebase-functions";
import Stripe from "stripe";

const stripe = new Stripe(functions.config().stripe.secret, {
  apiVersion: "2022-08-01",
});

const runtimeOpts: RuntimeOptions = {
  timeoutSeconds: 540,
  memory: "1GB",
};

export const createCheckoutSession = functions
  .region("asia-northeast1")
  .runWith(runtimeOpts)
  .https.onCall(async (data, context) => {
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: [
        {
          price: '{{PRICE_ID}}',
          quantity: 1,
        },
      ],
      mode: "subscription",
      success_url: "http://localhost:5173/",
      cancel_url: "http://localhost:5173/",
    });
    functions.logger.log(session);
    return session;
  });
```
> 参考URL
> https://stripe.com/docs/payments/checkout/migration?locale=ja-JP
> https://zenn.dev/sunaoproducts/articles/8c554c1bffc29b

最後にfunctionsをデプロイする
```bash:functions/
firebase deploy --only functions
```

### クライアント側の処理

```bash:client/
$ npm install --save @stripe/stripe-js && npm install --save firebase
```
firebaseを初期化
```javascript:firebaseInit.ts
import { initializeApp } from "firebase/app";
import { getFunctions } from "firebase/functions";

const firebaseConfig = {
.....
};

const firebaseApp = initializeApp(firebaseConfig);
// リージョンを東京に指定する。
const functions = getFunctions(firebaseApp, "asia-northeast1");

export { functions };

```
App.vueを記述

```vuejs:App.vue
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { loadStripe, Stripe } from "@stripe/stripe-js";
import { functions } from "./composables/firebase";
import { httpsCallable, HttpsCallableResult } from "firebase/functions";

let stripe: Stripe | null;

onMounted(async () => {
  stripe = await loadStripe(import.meta.env.VITE_STRIPE_API_KEY);
});

const pay = async () => {
  const createCheckoutSession = httpsCallable(functions, "createCheckoutSession");
  const result = await createCheckoutSession()
    .then((res: any) => { // TODO: responseの型定義をつける
      if (stripe) return stripe.redirectToCheckout({ sessionId: res.data.id });
      console.log(res);
    })
    .catch((e) => {
      console.log(e);
    });
};
</script>


<template>
  <button @click="pay">注文</button>
</template>

```

以上で`npm run dev`を実行して注文ボタンを押すとストライプの購入画面へリダイレクトされ、決済を組み込むことができた。

## Webhookによりイベントを取得
支払いが成功した場合やStripe側の処理が行われるとWebhookを通して自鯖に通知を行うことができる。
server.tsに次のような関数を追加。

:::note warn 
注意
実際に機能を実装するときは現在のStripeライブラリでは`event.data.object`の型定義が欠けているため自分で用意してあげる必要がある。

<details><summary>index.d.ts</summary>

```index.d.ts
declare module "stripe" {
  namespace Stripe {
    type DiscriminatedEvent =
      | DiscriminatedEvent.AccountApplicationEvent
      | DiscriminatedEvent.AccountExternalAccountEvent
      | DiscriminatedEvent.AccountEvent
      | DiscriminatedEvent.ApplicationFeeRefundEvent
      | DiscriminatedEvent.ApplicationFeeEvent
      | DiscriminatedEvent.BalanceEvent
      | DiscriminatedEvent.BillingPortalConfigurationEvent
      | DiscriminatedEvent.BillingPortalSessionEvent
      | DiscriminatedEvent.CapabilityEvent
      | DiscriminatedEvent.CashBalanceEvent
      | DiscriminatedEvent.ChargeDisputeEvent
      | DiscriminatedEvent.ChargeRefundEvent
      | DiscriminatedEvent.ChargeEvent
      | DiscriminatedEvent.CheckoutSessionEvent
      | DiscriminatedEvent.CouponEvent
      | DiscriminatedEvent.CreditNoteEvent
      | DiscriminatedEvent.CustomerDiscountEvent
      | DiscriminatedEvent.CustomerSourceEvent
      | DiscriminatedEvent.CustomerSubscriptionEvent
      | DiscriminatedEvent.CustomerTaxIdEvent
      | DiscriminatedEvent.CustomerEvent
      | DiscriminatedEvent.CustomerCashBalanceTransactionEvent
      | DiscriminatedEvent.FileEvent
      | DiscriminatedEvent.FinancialConnectionsAccountEvent
      | DiscriminatedEvent.IdentityVerificationSessionEvent
      | DiscriminatedEvent.InvoiceEvent
      | DiscriminatedEvent.InvoiceitemEvent
      | DiscriminatedEvent.IssuingAuthorizationEvent
      | DiscriminatedEvent.IssuingCardEvent
      | DiscriminatedEvent.IssuingCardholderEvent
      | DiscriminatedEvent.IssuingDisputeEvent
      | DiscriminatedEvent.IssuingTransactionEvent
      | DiscriminatedEvent.MandateEvent
      | DiscriminatedEvent.OrderEvent
      | DiscriminatedEvent.PaymentIntentEvent
      | DiscriminatedEvent.PaymentLinkEvent
      | DiscriminatedEvent.PaymentMethodEvent
      | DiscriminatedEvent.PayoutEvent
      | DiscriminatedEvent.PersonEvent
      | DiscriminatedEvent.PlanEvent
      | DiscriminatedEvent.PriceEvent
      | DiscriminatedEvent.ProductEvent
      | DiscriminatedEvent.PromotionCodeEvent
      | DiscriminatedEvent.QuoteEvent
      | DiscriminatedEvent.RadarEarlyFraudWarningEvent
      | DiscriminatedEvent.RecipientEvent
      | DiscriminatedEvent.ReportingReportRunEvent
      | DiscriminatedEvent.ReportingReportTypeEvent
      | DiscriminatedEvent.ReviewEvent
      | DiscriminatedEvent.SetupIntentEvent
      | DiscriminatedEvent.SigmaScheduledQueryRunEvent
      | DiscriminatedEvent.SkuEvent
      | DiscriminatedEvent.SourceTransactionEvent
      | DiscriminatedEvent.SourceEvent
      | DiscriminatedEvent.SubscriptionScheduleEvent
      | DiscriminatedEvent.TaxRateEvent
      | DiscriminatedEvent.TerminalReaderEvent
      | DiscriminatedEvent.TestHelpersTestClockEvent
      | DiscriminatedEvent.TopupEvent
      | DiscriminatedEvent.TransferEvent;

    namespace DiscriminatedEvent {
      /**
       * All possible event types: https://stripe.com/docs/api/events/types
       */
      type Type =
        | "account.updated"
        | "account.application.authorized"
        | "account.application.deauthorized"
        | "account.external_account.created"
        | "account.external_account.deleted"
        | "account.external_account.updated"
        | "application_fee.created"
        | "application_fee.refunded"
        | "application_fee.refund.updated"
        | "balance.available"
        | "billing_portal.configuration.created"
        | "billing_portal.configuration.updated"
        | "billing_portal.session.created"
        | "capability.updated"
        | "cash_balance.funds_available"
        | "charge.captured"
        | "charge.expired"
        | "charge.failed"
        | "charge.pending"
        | "charge.refunded"
        | "charge.succeeded"
        | "charge.updated"
        | "charge.dispute.closed"
        | "charge.dispute.created"
        | "charge.dispute.funds_reinstated"
        | "charge.dispute.funds_withdrawn"
        | "charge.dispute.updated"
        | "charge.refund.updated"
        | "checkout.session.async_payment_failed"
        | "checkout.session.async_payment_succeeded"
        | "checkout.session.completed"
        | "checkout.session.expired"
        | "coupon.created"
        | "coupon.deleted"
        | "coupon.updated"
        | "credit_note.created"
        | "credit_note.updated"
        | "credit_note.voided"
        | "customer.created"
        | "customer.deleted"
        | "customer.updated"
        | "customer.discount.created"
        | "customer.discount.deleted"
        | "customer.discount.updated"
        | "customer.source.created"
        | "customer.source.deleted"
        | "customer.source.expiring"
        | "customer.source.updated"
        | "customer.subscription.created"
        | "customer.subscription.deleted"
        | "customer.subscription.pending_update_applied"
        | "customer.subscription.pending_update_expired"
        | "customer.subscription.trial_will_end"
        | "customer.subscription.updated"
        | "customer.tax_id.created"
        | "customer.tax_id.deleted"
        | "customer.tax_id.updated"
        | "customer_cash_balance_transaction.created"
        | "file.created"
        | "financial_connections.account.created"
        | "financial_connections.account.deactivated"
        | "financial_connections.account.disconnected"
        | "financial_connections.account.reactivated"
        | "financial_connections.account.refreshed_balance"
        | "identity.verification_session.canceled"
        | "identity.verification_session.created"
        | "identity.verification_session.processing"
        | "identity.verification_session.redacted"
        | "identity.verification_session.requires_input"
        | "identity.verification_session.verified"
        | "invoice.created"
        | "invoice.deleted"
        | "invoice.finalization_failed"
        | "invoice.finalized"
        | "invoice.marked_uncollectible"
        | "invoice.paid"
        | "invoice.payment_action_required"
        | "invoice.payment_failed"
        | "invoice.payment_succeeded"
        | "invoice.sent"
        | "invoice.upcoming"
        | "invoice.updated"
        | "invoice.voided"
        | "invoiceitem.created"
        | "invoiceitem.deleted"
        | "invoiceitem.updated"
        | "issuing_authorization.created"
        | "issuing_authorization.request"
        | "issuing_authorization.updated"
        | "issuing_card.created"
        | "issuing_card.updated"
        | "issuing_cardholder.created"
        | "issuing_cardholder.updated"
        | "issuing_dispute.closed"
        | "issuing_dispute.created"
        | "issuing_dispute.funds_reinstated"
        | "issuing_dispute.submitted"
        | "issuing_dispute.updated"
        | "issuing_transaction.created"
        | "issuing_transaction.updated"
        | "mandate.updated"
        | "order.created"
        | "payment_intent.amount_capturable_updated"
        | "payment_intent.canceled"
        | "payment_intent.created"
        | "payment_intent.partially_funded"
        | "payment_intent.payment_failed"
        | "payment_intent.processing"
        | "payment_intent.requires_action"
        | "payment_intent.succeeded"
        | "payment_link.created"
        | "payment_link.updated"
        | "payment_method.attached"
        | "payment_method.automatically_updated"
        | "payment_method.detached"
        | "payment_method.updated"
        | "payout.canceled"
        | "payout.created"
        | "payout.failed"
        | "payout.paid"
        | "payout.updated"
        | "person.created"
        | "person.deleted"
        | "person.updated"
        | "plan.created"
        | "plan.deleted"
        | "plan.updated"
        | "price.created"
        | "price.deleted"
        | "price.updated"
        | "product.created"
        | "product.deleted"
        | "product.updated"
        | "promotion_code.created"
        | "promotion_code.updated"
        | "quote.accepted"
        | "quote.canceled"
        | "quote.created"
        | "quote.finalized"
        | "radar.early_fraud_warning.created"
        | "radar.early_fraud_warning.updated"
        | "recipient.created"
        | "recipient.deleted"
        | "recipient.updated"
        | "reporting.report_run.failed"
        | "reporting.report_run.succeeded"
        | "reporting.report_type.updated"
        | "review.closed"
        | "review.opened"
        | "setup_intent.canceled"
        | "setup_intent.created"
        | "setup_intent.requires_action"
        | "setup_intent.setup_failed"
        | "setup_intent.succeeded"
        | "sigma.scheduled_query_run.created"
        | "sku.created"
        | "sku.deleted"
        | "sku.updated"
        | "source.canceled"
        | "source.chargeable"
        | "source.failed"
        | "source.mandate_notification"
        | "source.refund_attributes_required"
        | "source.transaction.created"
        | "source.transaction.updated"
        | "subscription_schedule.aborted"
        | "subscription_schedule.canceled"
        | "subscription_schedule.completed"
        | "subscription_schedule.created"
        | "subscription_schedule.expiring"
        | "subscription_schedule.released"
        | "subscription_schedule.updated"
        | "tax_rate.created"
        | "tax_rate.updated"
        | "terminal.reader.action_failed"
        | "terminal.reader.action_succeeded"
        | "test_helpers.test_clock.advancing"
        | "test_helpers.test_clock.created"
        | "test_helpers.test_clock.deleted"
        | "test_helpers.test_clock.internal_failure"
        | "test_helpers.test_clock.ready"
        | "topup.canceled"
        | "topup.created"
        | "topup.failed"
        | "topup.reversed"
        | "topup.succeeded"
        | "transfer.created"
        | "transfer.reversed"
        | "transfer.updated";

      interface Data<T> {
        /**
         * Object containing the API resource relevant to the event. For example, an `invoice.created` event will have a full [invoice object](https://stripe.com/docs/api#invoice_object) as the value of the object key.
         */
        object: T;

        /**
         * Object containing the names of the attributes that have changed, and their previous values (sent along only with *.updated events).
         */
        previous_attributes?: Partial<T>;
      }

      interface AccountApplicationEvent extends Stripe.Event {
        type:
          | "account.application.authorized"
          | "account.application.deauthorized";
        data: DiscriminatedEvent.Data<"application">;
      }

      interface AccountExternalAccountEvent extends Stripe.Event {
        type:
          | "account.external_account.created"
          | "account.external_account.deleted"
          | "account.external_account.updated";
        data: DiscriminatedEvent.Data<Stripe.Card | Stripe.BankAccount>;
      }

      interface AccountEvent extends Stripe.Event {
        type: "account.updated";
        data: DiscriminatedEvent.Data<Stripe.Account>;
      }

      interface ApplicationFeeRefundEvent extends Stripe.Event {
        type: "application_fee.refund.updated";
        data: DiscriminatedEvent.Data<Stripe.FeeRefund>;
      }

      interface ApplicationFeeEvent extends Stripe.Event {
        type: "application_fee.created" | "application_fee.refunded";
        data: DiscriminatedEvent.Data<Stripe.ApplicationFee>;
      }

      interface BalanceEvent extends Stripe.Event {
        type: "balance.available";
        data: DiscriminatedEvent.Data<Stripe.Balance>;
      }

      interface BillingPortalConfigurationEvent extends Stripe.Event {
        type:
          | "billing_portal.configuration.created"
          | "billing_portal.configuration.updated";
        data: DiscriminatedEvent.Data<Stripe.BillingPortal.Configuration>;
      }

      interface BillingPortalSessionEvent extends Stripe.Event {
        type: "billing_portal.session.created";
        data: DiscriminatedEvent.Data<Stripe.BillingPortal.Session>;
      }

      interface CapabilityEvent extends Stripe.Event {
        type: "capability.updated";
        data: DiscriminatedEvent.Data<Stripe.Capability>;
      }

      interface CashBalanceEvent extends Stripe.Event {
        type: "cash_balance.funds_available";
        data: DiscriminatedEvent.Data<Stripe.CashBalance>;
      }

      interface ChargeDisputeEvent extends Stripe.Event {
        type:
          | "charge.dispute.closed"
          | "charge.dispute.created"
          | "charge.dispute.funds_reinstated"
          | "charge.dispute.funds_withdrawn"
          | "charge.dispute.updated";
        data: DiscriminatedEvent.Data<Stripe.Dispute>;
      }

      interface ChargeRefundEvent extends Stripe.Event {
        type: "charge.refund.updated";
        data: DiscriminatedEvent.Data<Stripe.Refund>;
      }

      interface ChargeEvent extends Stripe.Event {
        type:
          | "charge.captured"
          | "charge.expired"
          | "charge.failed"
          | "charge.pending"
          | "charge.refunded"
          | "charge.succeeded"
          | "charge.updated";
        data: DiscriminatedEvent.Data<Stripe.Charge>;
      }

      interface CheckoutSessionEvent extends Stripe.Event {
        type:
          | "checkout.session.async_payment_failed"
          | "checkout.session.async_payment_succeeded"
          | "checkout.session.completed"
          | "checkout.session.expired";
        data: DiscriminatedEvent.Data<Stripe.Checkout.Session>;
      }

      interface CouponEvent extends Stripe.Event {
        type: "coupon.created" | "coupon.deleted" | "coupon.updated";
        data: DiscriminatedEvent.Data<Stripe.Coupon>;
      }

      interface CreditNoteEvent extends Stripe.Event {
        type:
          | "credit_note.created"
          | "credit_note.updated"
          | "credit_note.voided";
        data: DiscriminatedEvent.Data<Stripe.CreditNote>;
      }

      interface CustomerDiscountEvent extends Stripe.Event {
        type:
          | "customer.discount.created"
          | "customer.discount.deleted"
          | "customer.discount.updated";
        data: DiscriminatedEvent.Data<Stripe.Discount>;
      }

      interface CustomerSourceEvent extends Stripe.Event {
        type:
          | "customer.source.created"
          | "customer.source.deleted"
          | "customer.source.expiring"
          | "customer.source.updated";
        data: DiscriminatedEvent.Data<Stripe.Card>;
      }

      interface CustomerSubscriptionEvent extends Stripe.Event {
        type:
          | "customer.subscription.created"
          | "customer.subscription.deleted"
          | "customer.subscription.pending_update_applied"
          | "customer.subscription.pending_update_expired"
          | "customer.subscription.trial_will_end"
          | "customer.subscription.updated";
        data: DiscriminatedEvent.Data<Stripe.Subscription>;
      }

      interface CustomerTaxIdEvent extends Stripe.Event {
        type:
          | "customer.tax_id.created"
          | "customer.tax_id.deleted"
          | "customer.tax_id.updated";
        data: DiscriminatedEvent.Data<Stripe.TaxId>;
      }

      interface CustomerEvent extends Stripe.Event {
        type: "customer.created" | "customer.deleted" | "customer.updated";
        data: DiscriminatedEvent.Data<Stripe.Customer>;
      }

      interface CustomerCashBalanceTransactionEvent extends Stripe.Event {
        type: "customer_cash_balance_transaction.created";
        data: DiscriminatedEvent.Data<Stripe.CustomerCashBalanceTransaction>;
      }

      interface FileEvent extends Stripe.Event {
        type: "file.created";
        data: DiscriminatedEvent.Data<Stripe.File>;
      }

      interface FinancialConnectionsAccountEvent extends Stripe.Event {
        type:
          | "financial_connections.account.created"
          | "financial_connections.account.deactivated"
          | "financial_connections.account.disconnected"
          | "financial_connections.account.reactivated"
          | "financial_connections.account.refreshed_balance";
        data: DiscriminatedEvent.Data<Stripe.Account>;
      }

      interface IdentityVerificationSessionEvent extends Stripe.Event {
        type:
          | "identity.verification_session.canceled"
          | "identity.verification_session.created"
          | "identity.verification_session.processing"
          | "identity.verification_session.redacted"
          | "identity.verification_session.requires_input"
          | "identity.verification_session.verified";
        data: DiscriminatedEvent.Data<Stripe.Identity.VerificationSession>;
      }

      interface InvoiceEvent extends Stripe.Event {
        type:
          | "invoice.created"
          | "invoice.deleted"
          | "invoice.finalization_failed"
          | "invoice.finalized"
          | "invoice.marked_uncollectible"
          | "invoice.paid"
          | "invoice.payment_action_required"
          | "invoice.payment_failed"
          | "invoice.payment_succeeded"
          | "invoice.sent"
          | "invoice.upcoming"
          | "invoice.updated"
          | "invoice.voided";
        data: DiscriminatedEvent.Data<Stripe.Invoice>;
      }

      interface InvoiceitemEvent extends Stripe.Event {
        type:
          | "invoiceitem.created"
          | "invoiceitem.deleted"
          | "invoiceitem.updated";
        data: DiscriminatedEvent.Data<Stripe.InvoiceItem>;
      }

      interface IssuingAuthorizationEvent extends Stripe.Event {
        type:
          | "issuing_authorization.created"
          | "issuing_authorization.request"
          | "issuing_authorization.updated";
        data: DiscriminatedEvent.Data<Stripe.Issuing.Authorization>;
      }

      interface IssuingCardEvent extends Stripe.Event {
        type: "issuing_card.created" | "issuing_card.updated";
        data: DiscriminatedEvent.Data<Stripe.Issuing.Card>;
      }

      interface IssuingCardholderEvent extends Stripe.Event {
        type: "issuing_cardholder.created" | "issuing_cardholder.updated";
        data: DiscriminatedEvent.Data<Stripe.Issuing.Cardholder>;
      }

      interface IssuingDisputeEvent extends Stripe.Event {
        type:
          | "issuing_dispute.closed"
          | "issuing_dispute.created"
          | "issuing_dispute.funds_reinstated"
          | "issuing_dispute.submitted"
          | "issuing_dispute.updated";
        data: DiscriminatedEvent.Data<Stripe.Issuing.Dispute>;
      }

      interface IssuingTransactionEvent extends Stripe.Event {
        type: "issuing_transaction.created" | "issuing_transaction.updated";
        data: DiscriminatedEvent.Data<Stripe.Issuing.Transaction>;
      }

      interface MandateEvent extends Stripe.Event {
        type: "mandate.updated";
        data: DiscriminatedEvent.Data<Stripe.Mandate>;
      }

      interface OrderEvent extends Stripe.Event {
        type: "order.created";
        data: DiscriminatedEvent.Data<Stripe.Order>;
      }

      interface PaymentIntentEvent extends Stripe.Event {
        type:
          | "payment_intent.amount_capturable_updated"
          | "payment_intent.canceled"
          | "payment_intent.created"
          | "payment_intent.partially_funded"
          | "payment_intent.payment_failed"
          | "payment_intent.processing"
          | "payment_intent.requires_action"
          | "payment_intent.succeeded";
        data: DiscriminatedEvent.Data<Stripe.PaymentIntent>;
      }

      interface PaymentLinkEvent extends Stripe.Event {
        type: "payment_link.created" | "payment_link.updated";
        data: DiscriminatedEvent.Data<Stripe.PaymentLink>;
      }

      interface PaymentMethodEvent extends Stripe.Event {
        type:
          | "payment_method.attached"
          | "payment_method.automatically_updated"
          | "payment_method.detached"
          | "payment_method.updated";
        data: DiscriminatedEvent.Data<Stripe.PaymentMethod>;
      }

      interface PayoutEvent extends Stripe.Event {
        type:
          | "payout.canceled"
          | "payout.created"
          | "payout.failed"
          | "payout.paid"
          | "payout.updated";
        data: DiscriminatedEvent.Data<Stripe.Payout>;
      }

      interface PersonEvent extends Stripe.Event {
        type: "person.created" | "person.deleted" | "person.updated";
        data: DiscriminatedEvent.Data<Stripe.Person>;
      }

      interface PlanEvent extends Stripe.Event {
        type: "plan.created" | "plan.deleted" | "plan.updated";
        data: DiscriminatedEvent.Data<Stripe.Plan>;
      }

      interface PriceEvent extends Stripe.Event {
        type: "price.created" | "price.deleted" | "price.updated";
        data: DiscriminatedEvent.Data<Stripe.Price>;
      }

      interface ProductEvent extends Stripe.Event {
        type: "product.created" | "product.deleted" | "product.updated";
        data: DiscriminatedEvent.Data<Stripe.Product>;
      }

      interface PromotionCodeEvent extends Stripe.Event {
        type: "promotion_code.created" | "promotion_code.updated";
        data: DiscriminatedEvent.Data<Stripe.PromotionCode>;
      }

      interface QuoteEvent extends Stripe.Event {
        type:
          | "quote.accepted"
          | "quote.canceled"
          | "quote.created"
          | "quote.finalized";
        data: DiscriminatedEvent.Data<Stripe.Quote>;
      }

      interface RadarEarlyFraudWarningEvent extends Stripe.Event {
        type:
          | "radar.early_fraud_warning.created"
          | "radar.early_fraud_warning.updated";
        data: DiscriminatedEvent.Data<Stripe.Radar.EarlyFraudWarning>;
      }

      interface RecipientEvent extends Stripe.Event {
        type: "recipient.created" | "recipient.deleted" | "recipient.updated";
        data: DiscriminatedEvent.Data<Stripe.Event.Data>;
      }

      interface ReportingReportRunEvent extends Stripe.Event {
        type: "reporting.report_run.failed" | "reporting.report_run.succeeded";
        data: DiscriminatedEvent.Data<Stripe.Reporting.ReportRun>;
      }

      interface ReportingReportTypeEvent extends Stripe.Event {
        type: "reporting.report_type.updated";
        data: DiscriminatedEvent.Data<Stripe.Reporting.ReportType>;
      }

      interface ReviewEvent extends Stripe.Event {
        type: "review.closed" | "review.opened";
        data: DiscriminatedEvent.Data<Stripe.Review>;
      }

      interface SetupIntentEvent extends Stripe.Event {
        type:
          | "setup_intent.canceled"
          | "setup_intent.created"
          | "setup_intent.requires_action"
          | "setup_intent.setup_failed"
          | "setup_intent.succeeded";
        data: DiscriminatedEvent.Data<Stripe.SetupIntent>;
      }

      interface SigmaScheduledQueryRunEvent extends Stripe.Event {
        type: "sigma.scheduled_query_run.created";
        data: DiscriminatedEvent.Data<Stripe.Sigma.ScheduledQueryRun>;
      }

      interface SkuEvent extends Stripe.Event {
        type: "sku.created" | "sku.deleted" | "sku.updated";
        data: DiscriminatedEvent.Data<Stripe.Sku>;
      }

      interface SourceTransactionEvent extends Stripe.Event {
        type: "source.transaction.created" | "source.transaction.updated";
        data: DiscriminatedEvent.Data<Stripe.SourceTransaction>;
      }

      interface SourceEvent extends Stripe.Event {
        type:
          | "source.canceled"
          | "source.chargeable"
          | "source.failed"
          | "source.mandate_notification"
          | "source.refund_attributes_required";
        data: DiscriminatedEvent.Data<Stripe.Card>;
      }

      interface SubscriptionScheduleEvent extends Stripe.Event {
        type:
          | "subscription_schedule.aborted"
          | "subscription_schedule.canceled"
          | "subscription_schedule.completed"
          | "subscription_schedule.created"
          | "subscription_schedule.expiring"
          | "subscription_schedule.released"
          | "subscription_schedule.updated";
        data: DiscriminatedEvent.Data<Stripe.SubscriptionSchedule>;
      }

      interface TaxRateEvent extends Stripe.Event {
        type: "tax_rate.created" | "tax_rate.updated";
        data: DiscriminatedEvent.Data<Stripe.TaxRate>;
      }

      interface TerminalReaderEvent extends Stripe.Event {
        type:
          | "terminal.reader.action_failed"
          | "terminal.reader.action_succeeded";
        data: DiscriminatedEvent.Data<Stripe.Terminal.Reader>;
      }

      interface TestHelpersTestClockEvent extends Stripe.Event {
        type:
          | "test_helpers.test_clock.advancing"
          | "test_helpers.test_clock.created"
          | "test_helpers.test_clock.deleted"
          | "test_helpers.test_clock.internal_failure"
          | "test_helpers.test_clock.ready";
        data: DiscriminatedEvent.Data<Stripe.TestHelpers.TestClock>;
      }

      interface TopupEvent extends Stripe.Event {
        type:
          | "topup.canceled"
          | "topup.created"
          | "topup.failed"
          | "topup.reversed"
          | "topup.succeeded";
        data: DiscriminatedEvent.Data<Stripe.Topup>;
      }

      interface TransferEvent extends Stripe.Event {
        type: "transfer.created" | "transfer.reversed" | "transfer.updated";
        data: DiscriminatedEvent.Data<Stripe.Transfer>;
      }
    }
  }
}
```
</details>

参考: https://github.com/stripe/stripe-node/issues/758
:::

```javascript:server.ts
export const stripeWebhook = functionOptions.https.onRequest(async (request, response) => {
  const endpointSecret: string = {{WEBHOOK_SECRET}};
  const signature = request.headers["stripe-signature"];

  let event;
  try {
    if (!signature) throw new Error("No signature provided");
    event = stripe.webhooks.constructEvent(request.rawBody, signature, endpointSecret) as Stripe.DiscriminatedEvent;
  } catch (e) {
    const err = e instanceof Error ? e : new Error("Bad Request");
    console.log(err);
    response.status(400).send(`Webhook Error: ${err.message}`);
    return;
  }

  let subscription;
  let status;
  switch (event.type) {
    case "customer.subscription.created":
      subscription = event.data.object;
      status = subscription.status;
      break;
    case "checkout.session.completed":
      subscription = event.data.object;
      status = subscription.status;
      functions.logger.log(status);
      break;
  }
  // functions.logger.log(event.type);
  response.status(200).send();
});

```

### ローカル環境でテストする
追加したコードをデプロイ

```bash:fucntions/
firebase deploy --only functions
```

#### ファイルの保存時毎回ビルドするようにしてホットリロードできるようにする。

```bash:fucntions/
npm run build -- --watch
```

#### ローカル環境で関数をエミュレートする

```bash:fucntions/
firebase serve --only functions
```

#### Stripe CLIを使ってローカル環境でWebhookを受け取れるようにする
> Stripe CLIのインストール
> https://stripe.com/docs/stripe-cli

```bash:fucntions/
stripe listen --forward-to http://localhost:5000/.../asia-northeast1/stripeWebhook
```

リッスンを実行した後に表示される**webhook secret key**を`{{WEBHOOK_SECRET}}`に置き換える。

#### 別のコンソールを開いて、Webhookの通知テストを行う。
```bash
stripe trigger checkout.session.completed
```

レスポンスで200番が帰ってきたらOK

### 次に本番環境でWebhookを登録する。
![screenshot-dashboard.stripe.com-2022.09.11-21_17_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/975119/5cc9d5df-1ac2-e496-68fa-d911f5631414.png)

エンドポイントURLにfunctionsにデプロイしたURLを記述してリッスンしたいイベントを適当に選んでイベントを追加を押す。
追加できたら、署名シークレットを取得しfirebaseにセットした後、serve.tsの{{WEBHOOK_SECRET}}を本番用に変更する。
署名シークレットを書き換えたらデプロイし、決済テストをしてwebhookを正常に動作出来たら完了

```bash:functions/
firebase deploy --only functions
```

```bash
stripe trigger checkout.session.completed
```