Building a Custom Shipping Calculator with Stripe and Netlify Functions for Multi-Currency (€/$), Quantity, and Location Support

IDMTR - Oct 31 - - Dev Community

Commit 3c90066

Before you read any further, just as an FYI, I learn and code on my own to build what we need to run our business. So, please take the following information as is. It's a real world example we used for our own 📙 yellow book about coworking. At the time we couldn't find a better solution, so I build the following for our eCommerce website.

Book about coworking around the world

Selling a single product online, like a book, can be straightforward until you encounter the complexities of international shipping rates, multiple currencies, and varying quantities—especially since Stripe Checkout allows for only one shipping rate by default. In this article, let's walk through how we built a custom shipping calculator using Netlify Functions and Stripe to handle these challenges. By the end, you'll have a working solution tailored for selling up to three copies of a book, with dynamic shipping costs based on the customer's currency (EUR/USD), quantity, and location.

While this example is very specific to our needs, you can tweak it to suit your own requirements. Please feel free to share your solutions, upgrades, or any improvements you make.

🚀 Prerequisites

Before we dive in, make sure you have the following:

  • A Netlify account with a deployed site.
  • A Stripe account with test and live API keys.
  • Basic understanding of HTML, JavaScript, and serverless functions.
  • Familiarity with environment variables.

📝 Overview

Let's create a seamless checkout experience that:

  • Determines shipping costs based on the customer's currency, number of items, and location.
  • Supports both EUR and USD currencies.
  • Handles different shipping rates for European and worldwide destinations.
  • Integrates seamlessly with Stripe Checkout.

Bellow I will cover both the frontend (HTML and JavaScript) and the backend (Netlify Function) components.

📁 Project Structure

Project should include the following folders and files:

/functions
  - create-checkout-session.js
/index.html
.env
netlify.toml
package.json
Enter fullscreen mode Exit fullscreen mode
  • /functions: Directory for Netlify Functions.
  • create-checkout-session.js: The custom serverless function.
  • index.html: The frontend HTML file.
  • .env: File to store environment variables
  • netlify.toml: The configuration file for Netlify.
  • package.json: Lists dependencies like stripe.

🛠️ Setting Up the Backend (Netlify Function)

Create a new file in your /functions directory named create-checkout-session.js.

// functions/create-checkout-session.js

// Add Stripe secret key
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

exports.handler = async (event) => {
  // Parse the order data sent from the frontend
  const order = JSON.parse(event.body);

  // Define country groups
  const euCountries = ['AL', 'AM', 'AT', ...]; // Add the EU countries you ship to
  const worldCountries = ['AE', 'AR', 'AU', ...]; // Add worldwide countries you ship to
  let allowedCountries = [];

  // Payment methods based on currency
  let paymentMethods = [];

  // Determine shipping rates and allowed countries
  if (order.currency === 'EUR') {
    paymentMethods = ['card', 'sepa_debit', 'ideal', 'bancontact', 'p24', 'eps', 'giropay', 'sofort'];

    if (order.shippingOption === 'europe-eur') {
      allowedCountries = euCountries;
      // Set shipping rate IDs for Europe in EUR
      order.shippingRate = process.env[`SHIPPING_RATE_EUR_EU_${order.items}`];
    } else if (order.shippingOption === 'world-eur') {
      allowedCountries = worldCountries;
      // Set shipping rate IDs for World in EUR
      order.shippingRate = process.env[`SHIPPING_RATE_EUR_W_${order.items}`];
    }
  } else if (order.currency === 'USD') {
    paymentMethods = ['card'];

    if (order.shippingOption === 'europe-usd') {
      allowedCountries = euCountries;
      // Set shipping rate IDs for Europe in USD
      order.shippingRate = process.env[`SHIPPING_RATE_USD_EU_${order.items}`];
    } else if (order.shippingOption === 'world-usd') {
      allowedCountries = worldCountries;
      // Set shipping rate IDs for World in USD
      order.shippingRate = process.env[`SHIPPING_RATE_USD_W_${order.items}`];
    }
  }

  // Create the Stripe Checkout session
  const session = await stripe.checkout.sessions.create({
    payment_method_types: paymentMethods,
    line_items: [
      {
        price: order.priceId, // The price ID of your product
        quantity: order.items,
      },
    ],
    mode: 'payment',
    billing_address_collection: 'auto',
    shipping_rates: [order.shippingRate],
    shipping_address_collection: {
      allowed_countries: allowedCountries,
    },
    success_url: `${process.env.URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.URL}/cancel`,
  });

  return {
    statusCode: 200,
    body: JSON.stringify({
      sessionId: session.id,
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
    }),
  };
};
Enter fullscreen mode Exit fullscreen mode

🔍 Code Breakdown

Importing Stripe

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
Enter fullscreen mode Exit fullscreen mode

Initializes the Stripe SDK with your secret key.

Handling the Event

Pars the incoming order data from the frontend.

exports.handler = async (event) => {
  const order = JSON.parse(event.body);
  // Rest of the code...
};
Enter fullscreen mode Exit fullscreen mode

Defining Country Groups

const euCountries = [/* ... */];
const worldCountries = [/* ... */];
let allowedCountries = [];
Enter fullscreen mode Exit fullscreen mode
  • Lists of countries for EU and worldwide shipping.
  • allowedCountries will be set based on the shipping option.

Setting Payment Methods

Determine the available payment methods based on the currency.

let paymentMethods = [];
Enter fullscreen mode Exit fullscreen mode

Determining Shipping Rates

if (order.currency === 'EUR') {
  paymentMethods = [/* ... */];

  if (order.shippingOption === 'europe-eur') {
    allowedCountries = euCountries;
    order.shippingRate = process.env[`SHIPPING_RATE_EUR_EU_${order.items}`];
  } else if (order.shippingOption === 'world-eur') {
    allowedCountries = worldCountries;
    order.shippingRate = process.env[`SHIPPING_RATE_EUR_W_${order.items}`];
  }
} else if (order.currency === 'USD') {
  // Similar logic for USD
}
Enter fullscreen mode Exit fullscreen mode
  • Uses environment variables to set the correct shipping rate ID based on currency, region, and quantity.
  • Example environment variable: SHIPPING_RATE_EUR_EU_1 for 1 item in Europe with EUR currency.

Creating the Checkout Session

const session = await stripe.checkout.sessions.create({
  payment_method_types: paymentMethods,
  line_items: [/* ... */],
  mode: 'payment',
  billing_address_collection: 'auto',
  shipping_rates: [order.shippingRate],
  shipping_address_collection: {
    allowed_countries: allowedCountries,
  },
  success_url: `${process.env.URL}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${process.env.URL}/cancel`,
});
Enter fullscreen mode Exit fullscreen mode
  • Creates a new Stripe Checkout session with dynamic configurations.

🛠️ Setting Up the Frontend

Below is a shortened example of the HTML and JavaScript code that interacts with our Netlify Function.

📄 HTML Structure (index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Book Pre-Order</title>
  <!-- Include any CSS or Meta tags here -->
</head>
<body>
  <!-- Book Purchase Section -->
  <section id="pricing">
    <div class="pricing-content">
      <!-- Currency Tabs -->
      <ul class="tabs-menu">
        <li id="active_currency_eur" class="current"><a href="#tab-1">Buy in 🇪🇺 EUR</a></li>
        <li id="active_currency"><a href="#tab-2">Buy in 🇺🇸 USD</a></li>
      </ul>

      <!-- EUR Tab Content -->
      <div id="tab-1" class="tab-content">
        <h3>1 Print Book</h3>
        <p>A beautiful, 350 pages book.</p>
        <p>Price: <span id="book-price-eur">€95</span></p>

        <!-- Number of Books -->
        <label for="num-books">Number of Books (Max 3)</label>
        <select name="num-books" id="num-books" required>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select>

        <!-- Shipping Destination -->
        <label for="shipping-amount-eur">Select Shipping Destination</label>
        <select name="shipping-amount" id="shipping-amount-eur" required>
          <optgroup label="Europe €14">
            <option value="europe-eur">Austria</option>
            <option value="europe-eur">Belgium</option>
            <!-- Add other European countries -->
          </optgroup>
          <optgroup label="Worldwide €22">
            <option value="world-eur">United States</option>
            <option value="world-eur">Canada</option>
            <!-- Add other worldwide countries -->
          </optgroup>
        </select>

        <!-- Checkout Button -->
        <button id="checkout-button-eur" type="button">PRE-ORDER</button>
      </div>

      <!-- USD Tab Content -->
      <div id="tab-2" class="tab-content">
        <h3>1 Print Book</h3>
        <p>A beautiful, 350 pages book.</p>
        <p>Price: <span id="book-price-usd">$99</span></p>

        <!-- Number of Books -->
        <label for="num-books-usd">Number of Books (Max 3)</label>
        <select name="num-books-usd" id="num-books-usd" required>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select>

        <!-- Shipping Destination -->
        <label for="shipping-amount-usd">Select Shipping Destination</label>
        <select name="shipping-amount" id="shipping-amount-usd" required>
          <optgroup label="Europe $16">
            <option value="europe-usd">Austria</option>
            <option value="europe-usd">Belgium</option>
            <!-- Add other European countries -->
          </optgroup>
          <optgroup label="Worldwide $22">
            <option value="world-usd">United States</option>
            <option value="world-usd">Canada</option>
            <!-- Add other worldwide countries -->
          </optgroup>
        </select>

        <!-- Checkout Button -->
        <button id="checkout-button-usd" type="button">PRE-ORDER</button>
      </div>
    </div>
  </section>

  <!-- Include Stripe.js -->
  <script src="https://js.stripe.com/v3/"></script>

  <!-- Include your JavaScript file -->
  <script src="script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

🔍 HTML Breakdown

  • Currency Tabs: Allows users to select between EUR and USD pricing.
  • Number of Books: Users can select up to three books.
  • Shipping Destination: Dropdowns populated with countries, grouped by shipping rates.
  • Checkout Buttons: Initiates the checkout process when clicked.

🚀 JavaScript Logic (script.js)

// script.js

// Stripe Button Click Event
document.querySelectorAll(".stripe-btn").forEach(function (button) {
  button.addEventListener("click", function () {
    if (button.classList.contains("stripe-btn")) {
      button.innerHTML = '<p>Loading ... <i class="icon-cart"></i></p>';
    }
  });
});

// Currency Tabs to switch between EUR and USD
document.addEventListener("DOMContentLoaded", function () {
  document.querySelectorAll(".tabs-menu a").forEach(function (tabLink) {
    tabLink.addEventListener("click", function (event) {
      event.preventDefault();
      // Toggle active class on tab buttons
      tabLink.parentElement.classList.add("current");
      tabLink.parentElement
        .parentElement
        .querySelectorAll(".current")
        .forEach((sibling) => {
          if (sibling !== tabLink.parentElement) {
            sibling.classList.remove("current");
          }
        });

      // Display only active tab content
      const activeTab = tabLink.getAttribute("href");
      document.querySelectorAll(".tab-content").forEach(function (content) {
        if (`#${content.id}` === activeTab) {
          content.style.display = "block";
        } else {
          content.style.display = "none";
        }
      });
    });
  });
});


// Event listeners for the checkout buttons
document.getElementById('checkout-button-eur').addEventListener('click', checkoutStripe);
document.getElementById('checkout-button-usd').addEventListener('click', checkoutStripe);

async function checkoutStripe(event) {
  let currency, shippingOption, numBooks, priceId;

  // Determine which button was clicked
  if (event.target.id === 'checkout-button-eur') {
    currency = 'EUR';
    shippingOption = document.getElementById('shipping-amount-eur').value;
    numBooks = parseInt(document.getElementById('num-books').value);
    priceId = 'PRICE_ID_EUR'; // Replace with your EUR price ID
  } else {
    currency = 'USD';
    shippingOption = document.getElementById('shipping-amount-usd').value;
    numBooks = parseInt(document.getElementById('num-books-usd').value);
    priceId = 'PRICE_ID_USD'; // Replace with your USD price ID
  }

  // Prepare the order data
  const orderData = {
    currency: currency,
    shippingOption: shippingOption,
    items: numBooks,
    priceId: priceId,
  };

  // Make a POST request to the Netlify Function
  const response = await fetch('/.netlify/functions/create-checkout-session', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(orderData),
  }).then((res) => res.json());

  // Redirect to Stripe Checkout
  const stripe = Stripe(response.publishableKey);
  const { error } = await stripe.redirectToCheckout({
    sessionId: response.sessionId,
  });

  if (error) {
    console.error(error);
    // Display error message to the user
  }
}
Enter fullscreen mode Exit fullscreen mode

🔍 JavaScript Breakdown

  • Event Listeners: Attach click events to the checkout buttons.
  • Determining Order Details: Based on the clicked button, extract the currency, shipping option, number of books, and price ID.
  • Preparing Order Data: Create an object containing all necessary order information.
  • Fetching the Checkout Session: Send a POST request to the Netlify Function with the order data.
  • Redirecting to Stripe Checkout: Use the session ID returned from the backend to redirect the user to Stripe Checkout.

🔑 Setting Environment Variables

Make sure to add your product and shipping prices on Stirpe Dashboard.

On Stripe:
Add stripe product and shipping prices
On Netlify:
Add variables to Netlify

Create a .env file in the root of your project and add your environment variables(or do it on the Netlify UI as shown above Site configuration > Environment variables):

# Stripe API keys
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...

# Your Netlify site URL
URL=https://your-netlify-site.netlify.app

# Price IDs from Stripe
PRICE_ID_EUR=price_1JNada...vxR5U
PRICE_ID_USD=price_1JNada...Swqw6

# Shipping rate IDs (replace with your actual IDs from Stripe), in our function, depending on the chosen number of books (1-3) those are dynamicly replaced.
SHIPPING_RATE_EUR_EU_1=shr_12345
SHIPPING_RATE_EUR_EU_2=shr_23456
SHIPPING_RATE_EUR_EU_3=shr_34567
SHIPPING_RATE_EUR_W_1=shr_45678
SHIPPING_RATE_EUR_W_2=shr_56789
SHIPPING_RATE_EUR_W_3=shr_67890

SHIPPING_RATE_USD_EU_1=shr_78901
SHIPPING_RATE_USD_EU_2=shr_89012
SHIPPING_RATE_USD_EU_3=shr_90123
SHIPPING_RATE_USD_W_1=shr_01234
SHIPPING_RATE_USD_W_2=shr_12345
SHIPPING_RATE_USD_W_3=shr_23456
Enter fullscreen mode Exit fullscreen mode
  • Replace the values with your actual Stripe keys and shipping rate IDs.
  • Make sure to create these shipping rates in your Stripe dashboard.

📝 Updating netlify.toml

Configure Netlify to use environment variables in your functions:

[build]
  functions = "functions/"

[functions]
  node_bundler = "esbuild"
Enter fullscreen mode Exit fullscreen mode

📦 Installing Dependencies

Run the following command to install the Stripe SDK:

npm install stripe
Enter fullscreen mode Exit fullscreen mode

🧪 Testing the Function

  1. Start Netlify Dev Server
   netlify dev
Enter fullscreen mode Exit fullscreen mode
  1. Place an Order
  • Open your index.html file in the browser.
  • Select your options and click the "PRE-ORDER" button.
  • Ensure that the correct shipping rates and payment methods appear in the Stripe Checkout.
  1. Test Different Scenarios
  • Switch between EUR and USD currencies.
  • Change the shipping options and item quantities.
  • Confirm that the allowed countries match your configurations.

🎉 Conclusion

Et voilà! You've set up a custom shipping calculator function that dynamically adjusts shipping rates based on currency, quantity, and location.

Feel free to adapt and expand upon this setup to suit your own products and shipping policies.

📚 Additional Resources


Note: This article is based on a real-world scenario for pre-ordering/selling a single book with up to three copies and demonstrates one way to handle shipping calculations involving currency, quantity, and location variables. There might be more efficient methods depending on your specific needs.

. . .