LoginSignup
2
1

More than 1 year has passed since last update.

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

Posted at

今回の構成

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

手順

サブスクリプションの実装

はじめに、funtionsフォルダへ移動してstripeをインストール

functions/
$ npm install --save stripe

firebaseで環境変数を構成

functions/
$ firebase functions:config:set stripe.secret=sk_test_51JK...

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

functions/
$ firebase functions:config:get

以下のような形で表示されていればOK

{
  "stripe": {
    "secret": "sk_test_51JK..."
  }
}

サーバー側のコードを書いていく

{{PRICE_ID}}にはStripeダッシュボードで事前に作成した商品のIDを記述する。

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をデプロイする

functions/
firebase deploy --only functions

クライアント側の処理

client/
$ npm install --save @stripe/stripe-js && npm install --save firebase

firebaseを初期化

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を記述

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に次のような関数を追加。

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

index.d.ts
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>;
      }
    }
  }
}

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

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();
});

ローカル環境でテストする

追加したコードをデプロイ

fucntions/
firebase deploy --only functions

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

fucntions/
npm run build -- --watch

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

fucntions/
firebase serve --only functions

Stripe CLIを使ってローカル環境でWebhookを受け取れるようにする

Stripe CLIのインストール
https://stripe.com/docs/stripe-cli

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

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

別のコンソールを開いて、Webhookの通知テストを行う。

stripe trigger checkout.session.completed

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

次に本番環境でWebhookを登録する。

screenshot-dashboard.stripe.com-2022.09.11-21_17_43.png

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

functions/
firebase deploy --only functions
stripe trigger checkout.session.completed
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1