Run Subscription Payments with Stripe Firebase Extension Webhook Not Firing

Viewed 2786

I added in the Stripe Subscriptions extension to my Firebase/Vue application so I can manage subscription plans for my customers, but I'm having problems with what I believe is the Stripe webhook.

I started off the extension installation by setting the firebase.rules like they said in the tutorial:

firebase.rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
    match /customers/{uid} {
      allow read, write: if request.auth.uid == uid;

      match /checkout_sessions/{id} {
        allow read, write: if request.auth.uid == uid;
      }
      match /subscriptions/{id} {
        allow read, write: if request.auth.uid == uid;
      }
    }

    match /products/{id} {
      allow read: if true;
      allow write: if false;

      match /prices/{id} {
        allow read: if true;
        allow write: if false;
      }
    }
  }
}

I configured the Stripe webhook like the following and added the webhook secret in the firebase extension: enter image description here

I also created a restricted key in the Stripe dashboard and added that to the extension as well.

When I test creating a new product from the Stripe dashboard, the webhook succeeds and a collection gets created in firebase:

Stripe Product dashboard: enter image description here

Firestore: enter image description here

Now, the problem:

The problem arises when I try to create and complete a Stripe checkout. From the tutorial, it looks like I need to do so call stripe.redirectToCheckout() and it'll handle most of the stuff for me, but I can't seem to find the sessionId of the checkout session. Here is the example they showed:

const docRef = await db
  .collection('customers')
  .doc(currentUser.uid)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });
// Wait for the CheckoutSession to get attached by the extension
docRef.onSnapshot((snap) => {
  const { sessionId } = snap.data();
  if (sessionId) {
    // We have a session, let's redirect to Checkout
    // Init Stripe
    const stripe = Stripe('pk_test_1234');
    stripe.redirectToCheckout({ sessionId });
  }
});

This is my implementation:

 async checkoutv2(item) {
      let currentUser = this.getCurrentUser();
      const stripe = window.Stripe(this.publishableKey);

      const docRef = await db
        .firestore()
        .collection("customers")
        .doc(currentUser.uid)
        .collection("checkout_sessions")
        .add({
          unit_amount: item["unit_amount"],
          plan: item["id"],
          description: item["description"],
          interval: item["interval"],
          currency: item["currency"],
          type: item["type"],
          interval_count: item["interval_count"],
          active: item["active"],
          // allow_promotion_codes: true,
          // tax_rates: ["txr_1HCshzHYgolSBA35WkPjzOOi"],
          success_url: window.location.origin,
          cancel_url: window.location.origin,
          mode: "subscription",
          metadata: {},
        });

      // Wait for the CheckoutSession to get attached by the extension
      docRef.onSnapshot((snap) => {
        const { sessionId } = snap.data();
        if (sessionId) {
          // We have a session, let's redirect to Checkout
          // Init Stripe
          stripe.redirectToCheckout({ sessionId });
        } else {
          console.log("NOPE");
        }
      });
    },

It seems like the CheckOutSession "attaches" to the extension after it's been added to the Firestore, and a session ID is created and added to the document. But I don't see that happening for my version. The checkout_sessions are being created, but with no sessionId.

enter image description here

Here is what snap data looks like: enter image description here

Has nothing on sessionId, but it does have it's own id, which I believe is the id that Firestore automatically creates for the collection.

Looking at my logs, no webhooks get triggered. When I test specific webhook events, some succeed and some don't.

Succeeds:

  • Anything to do with price, product creating/updating/deleting.
  • checkout.session.completed
  • customer.subscription.created

Fails (error status code 400):

  • customer.subscription.updated
  • customer.subscription.deleted (LOG: Webhook handler for Stripe event [evt_00000000000000] of type [customer.subscription.deleted] failed: No such subscription: 'sub_00000000000000')

Can anyone tell what's going on? I'm using Stripe API version: 2017-06-05. I've added the following to my index.html:

  <!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
    <script src="https://www.gstatic.com/firebasejs/7.14.6/firebase.js"></script>
    <script src="https://www.gstatic.com/firebasejs/7.14.6/firebase-functions.js"></script>

    <!-- If you enabled Analytics in your project, add the Firebase SDK for Analytics -->
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-analytics.js"></script>

    <!-- Add Firebase products that you want to use -->
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-firestore.js"></script>

    <script src="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.js"></script>
    <link
      type="text/css"
      rel="stylesheet"
      href="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.css"
    />

My package.json:

{
  "name": "mathpath",
  "version": "0.1.0",
  "private": true,
  "description": "## Project setup ``` npm install ```",
  "author": "",
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "start": "node index.js",
    "bd": "vue-cli-service build && firebase deploy"
  },
  "main": "babel.config.js",
  "dependencies": {
    "aws-sdk": "^2.719.0",
    "axios": "^0.19.2",
    "bootstrap": "^4.5.0",
    "bootstrap-vue": "^2.1.0",
    "core-js": "^3.6.5",
    "dns": "^0.2.2",
    "express": "^4.17.1",
    "firebase": "^7.17.1",
    "firebase-admin": "^9.0.0",
    "firebaseui": "^4.6.0",
    "fs": "0.0.1-security",
    "jquery": "^3.5.1",
    "katex": "^0.12.0",
    "markdown-it": "^11.0.0",
    "markdown-it-katex": "^2.0.3",
    "mathjax-node": "^2.1.1",
    "pg": "^8.2.2",
    "pg-hstore": "^2.3.3",
    "sequelize": "^6.3.0",
    "showdown": "^1.9.1",
    "showdown-katex": "^0.8.0",
    "slugify": "^1.4.5",
    "stripe": "^8.83.0",
    "uniqid": "^5.2.0",
    "vue": "^2.6.11",
    "vue-katex": "^0.4.0",
    "vue-router": "^3.3.4",
    "vue-stripe-checkout": "^3.5.7",
    "vue-youtube-embed": "^2.2.2",
    "vuefire": "^2.2.3",
    "vuex": "^3.5.1"
  },
  "devDependencies": {
    "@babel/polyfill": "^7.7.0",
    "@vue/cli-plugin-babel": "~4.4.0",
    "@vue/cli-plugin-eslint": "~4.4.0",
    "@vue/cli-service": "~4.4.0",
    "babel-eslint": "^10.1.0",
    "bootstrap": "^4.3.1",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "mutationobserver-shim": "^0.3.3",
    "popper.js": "^1.16.0",
    "portal-vue": "^2.1.6",
    "sass": "^1.19.0",
    "sass-loader": "^8.0.0",
    "vue-cli-plugin-bootstrap-vue": "~0.6.0",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {
      "no-console": "off",
      "no-restricted-syntax": [
        "error",
        {
          "selector": "CallExpression[callee.object.name='console'][callee.property.name!=/^(log|warn|error|info|trace)$/]",
          "message": "Unexpected property on console object was called"
        }
      ]
    }
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ],
  "license": "ISC"
}

3 Answers

I was running into this same issue.

Here's some things I changed before it started working, I'm not sure exactly what fixed it:

  • Shut down my firebase emulator and tested on production data. It appeared to be working in the emulator prior to the fix (except for the issues mentioned in the original post) but I haven't tested if it works in the emulator post-fix yet.
  • Deleted all of my test data and webhook in Stripe and started fresh.
  • Updated all my firebase and vue-stripe-checkout dependencies:

"firebase": "7.19.1", "firebase-admin": "^9.1.1", "firebase-functions": "^3.11.0", "firebase-tools": "^8.10.0", "firebaseui": "^4.6.1", "vue-stripe-checkout": "^3.5.7"

After all of this was done I created a test subscription product in Stripe and saw it appear in my products collection. This was already working prior to the fix. What was new was that I was now seeing a sessionId as part of the second snap.data() response. The first response looked just like the one prior to the "fix", and I thought it failed again but after waiting a second or two a new response appeared with the additional sessionId parameter

I'm also using Vue. I then take that session and commit it to update state. I watch for this session to change and then call the checkoutRef function. This time the user's collection gets updated with the new subscription.

For me this code is in store-auth.js

async checkoutUser({ commit }, payload) {
    let userId = firebaseAuth.currentUser.uid;

    const docRef = await firebaseDb
      .collection("users")
      .doc(userId)
      .collection("checkout_sessions")
      .add({
        price: "YOUR_PRICE_ID",
        success_url: window.location.origin,
        cancel_url: window.location.origin,
        mode: "subscription"
      });

    docRef.onSnapshot(snap => {
      const test = snap.data();

      const { sessionId } = snap.data();
      if (sessionId) {
        commit("setSession", sessionId);
      }
    });
  },

And this wherever you are using vue-stripe-checkout. For me it's on PagePlans.vue

    <stripe-checkout
    ref="checkoutRef"
    :pk="publishableKey"
    :mode="mode"
    :items="items"
    :successUrl="successUrl"
    :cancelUrl="cancelUrl"
    :session-id="sessionId"

  watch: {
    currentSession: {
      handler(newValue, oldValue) {
        this.sessionId = newValue

        this.$refs.checkoutRef.redirectToCheckout({scenarioId: newValue.sessionId})
      }
    }
  },
  computed: {
    ...mapGetters("auth", ["currentSession"]),
  },
  methods: {
    ...mapActions("auth", [
      "checkoutUser",
    ]),
    async checkout() {
      await this.checkoutUser()
    }
  }

For Firebase cloud functions, I took a look in the logs and saw it was an access problem.

Edit your restricted API key and allow Customers permission to

Stripe Restricted API permissions:
Stripe Restricted API permissions

Firebase Function for Stripe:
Firebase Function for Stripe

I had this same problem using the stripe firebase extension. In the stripe firebase extension configuration put the checkout_sessions inside the customers collection, and in my code I was using the users collection.

Started working once I used users in both places.

Related