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.
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
-
/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 likestripe
.
🛠️ 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,
}),
};
};
🔍 Code Breakdown
Importing Stripe
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
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...
};
Defining Country Groups
const euCountries = [/* ... */];
const worldCountries = [/* ... */];
let allowedCountries = [];
- 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 = [];
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
}
- 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`,
});
- 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>
🔍 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
}
}
🔍 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.
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
- 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"
📦 Installing Dependencies
Run the following command to install the Stripe SDK:
npm install stripe
🧪 Testing the Function
- Start Netlify Dev Server
netlify dev
- 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.
- 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
- Stripe Checkout Documentation
- Netlify Functions Documentation
- Creating Shipping Rates in Stripe
- Stripe.js Reference
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.
Top comments (0)