Znote (recipes)
  Get Znote  

Payment Stripe + Airtable

Deploy a Stripe webhook server to manage post-payment actions with Airtable

 

Payment Stripe + Airtable

Install NPM Packages

npm i -S stripe
npm i -S express
npm i -S cors
npm i -S body-parser
npm i -S node-fetch@2.6.6
npm i -S nodemailer
npm i -S jsonwebtoken

Gmail config

Install and configure the Gmail plugin (Bottom right button)

Copy the gmail_app_password: XXXXXX


Airtable

Create and save a Token

  1. https://airtable.com/create/tokens Scopes: data.records:read data.records:write schema.bases:read schema.bases:write Access: Your workspace
  2. Create a new Table Info: The Airtable URL is composed like this: https://airtable.com/BASE/TABLE/VIEW Example: https://airtable.com/appnXXXX/tblXXXX/viwXXX Initialize the table with the schema given in the note (👋)
  3. Run Write Airtable-api.js in the Airtable note.

Copy the airtable_base: "appXXX" Copy the airtable_token: XXXXX Copy the airtable_table: tblXXX


Stripe initialization for Dev

  1. Create a product and add a price https://dashboard.stripe.com/test/products?active=true

Copy the test price id product1_price: price_XXXXXX

  1. Generate an API key (in TEST mode): https://dashboard.stripe.com/test/apikeys

Copy the test stripe_key: sk_test_XXXXX

  1. Install the command line tool to listen to events locally https://stripe.com/docs/stripe-cli Open an external terminal, install and run the Stripe command line tool
stripe login
stripe listen --forward-to localhost:4242/webhook

Copy the test stripe_ws_secret: whsec_XXXXX

  1. Create a Payment link https://dashboard.stripe.com/test/payment-links/create (If you do not see events after a test payment, restart the command line tool) Create a metadata parameter: product with a value of your choice to identify the purchased product

Success payment: Card Number test: 4242 4242 4242 4242 Expiration Date: Use a future date CVC: 000

Error with insufficient_funds: Card Number test: 4000 0000 0000 9995

You can also manually trigger an event with a demo response:

stripe trigger checkout.session.completed

Stripe initialization for Prod

  1. Generate an API key (in PROD mode): https://dashboard.stripe.com/apikeys

Copy the live stripe_key: sk_live_XXXXX

  1. Create a webhook endpoint (in PROD mode): https://dashboard.stripe.com/webhooks
  • Leave endpoint URL empty until you get your Znote URL after first deployment
  • Add webhook events to listen in your app:
    • checkout.session.async_payment_failed
    • checkout.session.async_payment_succeeded
    • checkout.session.completed

Copy the live stripe_ws_secret: whsec_XXXXX

  1. Create a product and add a price https://dashboard.stripe.com/test/products?active=true

Copy the live product1_price: price_XXXXX

  1. Create a Payment link https://dashboard.stripe.com/test/payment-links/create Create a metadata parameter: product with a value of your choice to identify the purchased product

Credentials file

Credential file credentials-properties.json

{
  "stripe_key": "sk_test_XXX",
  "stripe_ws_secret": "whsec_XXX",
  "url_domain": "http://localhost:4242",
  "product1_price": "price_XXXXX",
  "gmail_app_password": "XXXXX",
  "airtable_base": "appXXXXX",
  "airtable_token": "XXXXXXX",
  "airtable_table": "tblXXXXXX"
}
{
  "stripe_key": "sk_live_XXXXX",
  "stripe_ws_secret": "whsec_XXXXX",
  "url_domain": "http://worker1.znote.io/XXXXX/",
  "product1_price": "price_XXXXX",
  "gmail_app_password": "XXXXX",
  "airtable_base": "appXXXXX",
  "airtable_token": "XXXXXXX",
  "airtable_table": "tblXXXXXX"
}

Write credential files above

const credentials = loadBlock('credentials-dev');
_fs.writeFileSync(`${__dirname}/credentials-dev-properties.json`, credentials);
const credentials = loadBlock('credentials-prod');
_fs.writeFileSync(`${__dirname}/credentials-prod-properties.json`, credentials);

Open the Zenv folder and copy your downloadable content

open .

Stripe backend

//exec: node
const properties = require("./credentials-dev-properties.json");

const fetch = require('node-fetch');
const stripe = require('stripe')(properties.stripe_key);
const express = require('express');
var jwt = require('jsonwebtoken');
const cors = require('cors')
const bodyParser = require('body-parser');
const nodemailer = require('nodemailer');
const Airtable = require("./airtable-api.js");

// Init Airtable
const BASE = properties.airtable_base;
const TOKEN = properties.airtable_token;
const TABLE = properties.airtable_table;
const airtable = new Airtable(TOKEN, BASE);

const MAX_DOWNLOAD_COUNT = 5; // Download limit
const JWT_SECRET = "CHANGE_ME"; // Secret to sign the link sent to client
const DOWNLOADABLE_FILE = "MY_EBOOK.pdf"; // Content file to download

const app = express();
app.use(cors());

const YOUR_DOMAIN = properties.url_domain;
// Your endpoint secret from your Stripe dashboard webhook settings
const endpointSecret = properties.stripe_ws_secret;

app.get('/', async (request, response) => {
  response.sendStatus(200);
});

app.get('/download/*', async (request, response) => {
  try {
    const token = request.params[0].split("/")[0];
    const decoded = jwt.verify(token, JWT_SECRET);
    const email = decoded.email;
    console.log(email);
    const result = await airtable.searchRecord(TABLE, 'SEARCH(SUBSTITUTE("'+email+'","+","%2B"), {email})');
    if (result.records.length !== 0) {
      const userId = result.records[0].id;
      const nbrOfDownload = result.records[0].fields.nbrOfDownload;
      if (nbrOfDownload <= MAX_DOWNLOAD_COUNT) {
        // Increment the download count for the user
        let userDownloads = nbrOfDownload + 1;
        const data = {
          "fields": {
            "nbrOfDownload": userDownloads
          }
        };
        const result = await airtable.updateRecord(TABLE, userId, data);
        printJSON(result);
        // Return downloadable content
        const file = `./${DOWNLOADABLE_FILE}`;
        return response.download(file);
      }
    }
    return response.status(403).send("You couldn't download this content");
  } catch(err) {
    console.log(err);
    return response.sendStatus(500);
  }
});

/**
 * Process after payment: Generate license
 */
app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (request, response) => {
  const payload = request.body;
  const sig = request.headers['stripe-signature'];

  let event;
  try {
    event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
  } catch (err) {
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }

  const session = event.data.object;

  console.log(event.type);

  switch (event.type) {
    case 'checkout.session.completed':
      // Fulfill order
      const creationDate = new Date().toISOString().split('T')[0];
      const product = session.metadata.product;
      const email = session.customer_details.email;
      console.log(product);
      console.log(email);

      // Compute link
      const token = jwt.sign({ email: email, product: product }, JWT_SECRET);
      const link = YOUR_DOMAIN + "/download/" + token;

      // Create customer record in Airtable
      try {
        const data = {
          "records": [
            {
              "fields": {
                "Email": email,
                "purchaseDate": creationDate,
                "nbrOfDownload": 0,
                "version": 1
              }
            }
          ]
        };
        const result = await airtable.createRecord(TABLE, data);
        printJSON(result);
      } catch(err) {
        print(err); // Log Airtable error if any
      }

      // Send Thank you email with download link
      const sender = "lagrede.anthony@gmail.com";
      const recipient = email;
      const subject = "Thank you for your purchase 😊";
      const content = `
        Hi 👋, <br/>Thank you for your purchase!<br/><br/>
        Here is your link to download your content: ${link} <br/><br/>
        You have ${MAX_DOWNLOAD_COUNT} downloads left.<br/><br/>
        Best regards<br/>
        The Znote Team
      `;

      sendMailWithGmail(sender, recipient, subject, content);
      print("✅ Email sent successfully!");

      break;
    default:
    // Unhandled event type
  }
  response.sendStatus(200);
});

app.listen(4242, () => console.log('Running on port 4242'));

Related recipes