Adding a form to an ecommerce site and sending data to Sanity and Stripe

8 replies
Last updated: Jul 6, 2022
Hey guys, I am admittedly a total noob with the back end. Heck, I am even pretty new to JS in general. I am using React for the first time as well on this project. I have a simple question with hopefully a simple solution. Bonus points if you explain it assuming I am 5 years old. So my issue is this. I currently have an ecommerce shop that sells customizable goods (customizable text, color, etc) all set up through sanity, nextjs, and react... I want to add a simple two or three required text fields form to a product page (probably with react hook form) above two buttons (these buttons pass state to my cart component). I need this form to send the text the customer typed in the field back to the same sanity studio where I push out products to the database (if possible?) so I can view the customer's order requests they input into the form externally (is there a way to have only the successful orders from stripe send back the custom data into the sanity studio?)
Within my ecommerce page is a dynamic single product page where you find the typical things: pictures of product, title of product, description of product all of which are pulled dynamically from the slug. Under the description I have the quantity and then under this I have the add to cart and buy now buttons. The buy now button just adds to cart and opens the cart sidebar component. Within the cart sidebar component there is a proceed to payment button which redirects them to stripes checkout page. This page will redirect them back to either my success or cancel page.
AI Update

Alright, let me break this down into simple steps for you! You're building an awesome customizable e-commerce shop, and you want to collect customer customization data (like text they want on products) and save it to Sanity. Let me explain this like you're five, then give you the technical details.

The Simple Explanation

Think of it like this: When someone orders a custom birthday cake, they need to tell the baker what text to write on it. You need to:

  1. Give them a form to write their custom text
  2. Save that text somewhere you can see it later (Sanity)
  3. Only save it if they actually paid (successful Stripe payment)

How To Do This

You have two main approaches:

This is what the community answer you found suggests. Here's the flow:

  1. Add form fields to your product page - Collect the custom text right where you are now (on the product detail page)
  2. Attach that data to the product in your cart - When they click "Add to Cart", include the custom options
  3. Send everything to Stripe - Pass the product info AND custom options to Stripe checkout
  4. Use a Stripe webhook - When payment succeeds, Stripe tells your server
  5. Save to Sanity - Your server then creates a document in Sanity with the order details

The key insight from that thread: You can add custom data to your product object before adding it to cart, like:

const [options, setOptions] = useState({optionOne: '', optionTwo: ''});
product.options = options; // Attach custom data to product

Since you mentioned you're new to backend stuff, I should tell you about Sanity Functions - these are serverless functions that run on Sanity's infrastructure, so you don't need to manage your own server! This is actually easier than webhooks for your use case.

Step-by-Step Implementation

Here's what you need to do:

Step 1: Add form fields to collect custom data

On your product page, add text inputs:

<div>
  <label>Custom Text Line 1</label>
  <input 
    type='text' 
    onChange={(e) => setCustomText({...customText, line1: e.target.value})}
  />
  <label>Custom Text Line 2</label>
  <input 
    type='text' 
    onChange={(e) => setCustomText({...customText, line2: e.target.value})}
  />
</div>

Step 2: Attach this data to your cart items

When adding to cart, include the custom data with the product.

Step 3: Set up a Stripe webhook

Stripe webhooks notify your server when payments succeed. You'll need to:

  • Create an API endpoint (like /api/stripe-webhook)
  • Register this URL in your Stripe dashboard
  • Listen for the checkout.session.completed event

Step 4: Create an order document in Sanity

When Stripe confirms payment, use the Sanity Client to create a document:

import {createClient} from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'production',
  token: 'your-write-token', // Keep this secret!
  apiVersion: '2024-01-01',
})

// In your webhook handler
await client.create({
  _type: 'order',
  customerEmail: session.customer_email,
  items: session.line_items,
  customizations: customData,
  stripeSessionId: session.id,
  status: 'paid'
})

Important Security Note

Never expose write tokens in your frontend code! The token that lets you create documents in Sanity must stay on your server. This is why you need the webhook approach - it runs on your server (or in a serverless function) where the token is safe.

You can create a token with write permissions in Sanity Manage under the API tab. Store it in environment variables.

Defining an Order Schema in Sanity

In your Sanity Studio, create a schema for orders:

export default {
  name: 'order',
  type: 'document',
  title: 'Order',
  fields: [
    {name: 'customerEmail', type: 'string'},
    {name: 'stripeSessionId', type: 'string'},
    {
      name: 'customizations',
      type: 'object',
      fields: [
        {name: 'line1', type: 'string'},
        {name: 'line2', type: 'string'},
      ]
    },
    {name: 'status', type: 'string'},
    // Add other fields as needed
  ]
}

Alternative: Sanity Functions Instead of Webhooks

If managing a webhook endpoint sounds complicated, consider using Sanity Functions. However, for Stripe integration specifically, webhooks are still the standard approach since Stripe needs to call YOUR endpoint when payment succeeds.

Resources

You're doing great tackling this as your first React project! The learning curve is steep but you're asking all the right questions. Good luck! 🎉

You could setup a webhook in stripe to trigger on successful payments https://stripe.com/docs/webhooks .. Stripe should send your order data in the body of the request, use that to patch in the data into sanity https://www.sanity.io/docs/http-mutations
Thank you so much!!! That actually helps a lot. But how could I collect this data and send it to stripe? I am still a bit confused on how to actually implement the forms for asking what text they would like on their order, etc?
user X
If it helps at all, this is the data structure I mimicked since a lot of this is new to me https://github.com/adrianhajdin/ecommerce_sanity_stripe
user N
Heres a quick way to do in your /product/[slug] page...in sanity create an object for your product options, then patch in the data. You will probably have to return the options object in your stripe.js file too
import React, { useState } from 'react';
import { AiOutlineMinus, AiOutlinePlus, AiFillStar, AiOutlineStar } from 'react-icons/ai';

import { client, urlFor } from '../../lib/client';
import { Product } from '../../components';
import { useStateContext } from '../../context/StateContext';

const ProductDetails = ({ product, products }) => {
  const [options, setOptions] = useState({optionOne: '', optionTwo: ''});
  const { image, name, details, price, } = product;
  product.options = options
  const [index, setIndex] = useState(0);
  const { decQty, incQty, qty, onAdd, setShowCart } = useStateContext();

  console.log(product)
  const handleBuyNow = () => {
    onAdd(product, qty);

    setShowCart(true);
  }

  return (
    <div>
      <div className="product-detail-container">
        <div>
          <div className="image-container">
            <img src={urlFor(image && image[index])} className="product-detail-image" />
          </div>
          <div className="small-images-container">
            {image?.map((item, i) => (
              <img 
                key={i}
                src={urlFor(item)}
                className={i === index ? 'small-image selected-image' : 'small-image'}
                onMouseEnter={() => setIndex(i)}
              />
            ))}
          </div>
        </div>

        <div className="product-detail-desc">
          <h1>{name}</h1>
          <div className="reviews">
            <div>
              <AiFillStar />
              <AiFillStar />
              <AiFillStar />
              <AiFillStar />
              <AiOutlineStar />
            </div>
            <p>
              (20)
            </p>
          </div>
          <h4>Details: </h4>
          <p>{details}</p>
          <p className="price">${price}</p>
          <div className="quantity">
            <h3>Quantity:</h3>
            <p className="quantity-desc">
              <span className="minus" onClick={decQty}><AiOutlineMinus /></span>
              <span className="num">{qty}</span>
              <span className="plus" onClick={incQty}><AiOutlinePlus /></span>
            </p>
          </div>
          <div>
            <label htmlFor='option1'>Option 1</label><input type='text' name='optionOne' onChange={(e) => setOptions({...options, optionOne: e.target.value})}/>
            <label htmlFor='option1'>Option 2</label><input type='text' name='optionTwo' onChange={(e) => setOptions({...options, optionTwo: e.target.value})}/>
          </div>
          <div className="buttons">
            <button type="button" className="add-to-cart" onClick={() => onAdd(product, qty)}>Add to Cart</button>
            <button type="button" className="buy-now" onClick={handleBuyNow}>Buy Now</button>
          </div>
        </div>
      </div>

      <div className="maylike-products-wrapper">
          <h2>You may also like</h2>
          <div className="marquee">
            <div className="maylike-products-container track">
              {products.map((item) => (
                <Product key={item._id} product={item} />
              ))}
            </div>
          </div>
      </div>
    </div>
  )
}

export const getStaticPaths = async () => {
  const query = `*[_type == "product"] {
    slug {
      current
    }
  }
  `;

  const products = await client.fetch(query);

  const paths = products.map((product) => ({
    params: { 
      slug: product.slug.current
    }
  }));

  return {
    paths,
    fallback: 'blocking'
  }
}

export const getStaticProps = async ({ params: { slug }}) => {
  const query = `*[_type == "product" && slug.current == '${slug}'][0]`;
  const productsQuery = '*[_type == "product"]'
  
  const product = await client.fetch(query);
  const products = await client.fetch(productsQuery);

  console.log(product);

  return {
    props: { products, product }
  }
}

export default ProductDetails

stripe.js 
return {
            price_data: { 
              currency: 'usd',
              product_data: { 
                name: item.name,
                images: [newImage],
              },
              unit_amount: item.price * 100,
            },
            adjustable_quantity: {
              enabled:true,
              minimum: 1,
            },
            quantity: item.quantity,
            options: item.options
          }
user N
New to JavaScript and React and you chose to make a headless shop? My friend, you are my hero of the day! 😛
LOL well, it all started out by me underestimating the complexity behind processing payments securely. Built an entirely static site that was gorgeous but without a tutorial for all of the backend stuff I was helplessly lost on how to execute what I needed with best practice. So I scrapped my progress and started fresh following a youtube tutorial for this. I have officially gone down the rabbit hole, but I have learned a TON and am so excited to get a better understanding of all the fundamentals now that I seen the whole process in action! Struggling through foreign coding languages has been surprisingly addicting haha
user S
I totally get it. I do the seven stages of grief for every major project but I'm still here so something must be keeping me in the mix!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?