5

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"
}

averageUsername123
  • 725
  • 2
  • 10
  • 22
  • Is your callback for `docRef.onSnapshot()` being called at all? If so, is there anything actually in `snap` if you log it out? – taintedzodiac Aug 11 '20 at 17:34
  • @taintedzodiac good question, yes I am getting back data in` snap`. I've updated my question to include a console.log of it. – averageUsername123 Aug 11 '20 at 17:37
  • Thanks for that! It does indeed appear that the sessionId isn't being stored in Firebase, and thus you can't redirect to it. This is all before the webhooks get involved. Your app is creating CheckoutSessions successfully, so the issue appears to be that the Firebase collection isn't saving the id's properly. – taintedzodiac Aug 11 '20 at 17:59
  • @taintedzodiac Right, I don't get in the example how after adding a checkout_session collection, that a sessionId would exist. The comment `// Wait for the CheckoutSession to get attached by the extension` makes me believe that something happens in the backend that triggers a webhook to insert a sessionId to the collection, but maybe I'm wrong. – averageUsername123 Aug 11 '20 at 18:03
  • It's not clear to me what might be causing the issue here. Might be worth filing an Issue on https://github.com/stripe-samples/firebase-subscription-payments since it could be in the extension itself. – taintedzodiac Aug 11 '20 at 18:04
  • Did you find a sollution yet? Because I'm having the same problem.. – Gijs van Beusekom Aug 31 '20 at 20:00
  • @averageUsername123 how you set product and price in firebase? are you inserting manually products from stripe? or created cron to get products and price from stripe? – Jitendra Sep 21 '20 at 12:07

3 Answers3

1

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

ielvis
  • 11
  • 2
  • @ielivs how you set product and price in firebase? are you inserting manually products from stripe? or created cron to get products and price from stripe? – Jitendra Sep 21 '20 at 12:09
  • 1
    @Jitendra, after I went through the steps in my post the product/price get's automatically added as a new collection in firebase when I create/update it through Stripe's dashboard. I manually create the product/prices in Stripe and the plugin creates/updates the collections/documents in Firebase automatically now that I have it working. – ielvis Sep 24 '20 at 17:34
0

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

Dharman
  • 30,962
  • 25
  • 85
  • 135
culco
  • 21
  • 1
0

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.

justinmoon
  • 443
  • 5
  • 11