Commerce JS API error

I’m having a tricky problem. I’ve created this ecommerce platform with react and I’m using the commerce JS API. I connected my sandbox stripe API key with the sandbox commerce JS key so I could do a test transaction, but each time I get an error and the transaction doesn’t go through, leaving my cart quantity unchanged.

In console I get the following error from the commerce JS API:

According to the commerce JS developer setting logs this is the request I’m sending:

{
  "line_items": [
    {
      "id": "item_7RyWOwmK5nEa2V",
      "product_id": "prod_LvJjoPJGale0nO",
      "name": "Ferrari",
      "product_name": "Ferrari",
      "media": {
        "type": "image",
        "source": "https://cdn.chec.io/merchants/28537/assets/o0wPb0EDWAlJnEV1|1.jpg"
      },
      "sku": null,
      "permalink": "kKwz9a",
      "quantity": 1,
      "price": {
        "raw": 1.5,
        "formatted": "1.50",
        "formatted_with_symbol": "£1.50",
        "formatted_with_code": "1.50 GBP"
      },
      "line_total": {
        "raw": 1.5,
        "formatted": "1.50",
        "formatted_with_symbol": "£1.50",
        "formatted_with_code": "1.50 GBP"
      },
      "is_valid": true,
      "product_meta": [],
      "tax": {
        "is_taxable": false,
        "taxable_amount": null,
        "amount": null,
        "breakdown": null
      },
      "selected_options": [],
      "variant": []
    }
  ],
  "customer": [],
  "shipping": {
    "name": "Primary",
    "county_state": "WSM",
    "country": "GB"
  },
  "fulfillment": {
    "shipping_method": "ship_RqEv5xvDqwZz4j"
  },
  "payment": {
    "gateway": "stripe",
    "stripe": {
      "payment_method_id": "pm_1J1FitHjVwDyRBqGNRnw8Ryt"
    }
  }
} 

I don’t understand how the email field, shipping address and shipping town/city are missing as they are all in my checkout form and I filled in everything accurately.

Here is my AddressForm component:

import React, { useState, useEffect } from "react";
import {
  InputLabel,
  Select,
  MenuItem,
  Button,
  Grid,
  Typography,
} from "@material-ui/core";
import { useForm, FormProvider } from "react-hook-form";
import { Link } from "react-router-dom";

import { commerce } from "../../lib/commerce";
import FormInput from "./CustomTextField";

const AddressForm = ({ checkoutToken, next }) => {
  const [shippingCountries, setShippingCountries] = useState([]);
  const [shippingCountry, setShippingCountry] = useState("");
  const [shippingSubdivisions, setShippingSubdivisions] = useState([]);
  const [shippingSubdivision, setShippingSubdivision] = useState("");
  const [shippingOptions, setShippingOptions] = useState([]);
  const [shippingOption, setShippingOption] = useState("");
  const methods = useForm();

  const fetchShippingCountries = async (checkoutTokenId) => {
    const { countries } = await commerce.services.localeListShippingCountries(
      checkoutTokenId
    );

    setShippingCountries(countries);
    setShippingCountry(Object.keys(countries)[0]);
  };

  const fetchSubdivisions = async (countryCode) => {
    const { subdivisions } = await commerce.services.localeListSubdivisions(
      countryCode
    );

    setShippingSubdivisions(subdivisions);
    setShippingSubdivision(Object.keys(subdivisions)[0]);
  };

  const fetchShippingOptions = async (
    checkoutTokenId,
    country,
    region = null
  ) => {
    const options = await commerce.checkout.getShippingOptions(
      checkoutTokenId,
      { country, region }
    );

    setShippingOptions(options);
    setShippingOption(options[0].id);
  };

  useEffect(() => {
    fetchShippingCountries(checkoutToken.id);
  }, []);

  useEffect(() => {
    if (shippingCountry) fetchSubdivisions(shippingCountry);
  }, [shippingCountry]);

  useEffect(() => {
    if (shippingSubdivision)
      fetchShippingOptions(
        checkoutToken.id,
        shippingCountry,
        shippingSubdivision
      );
  }, [shippingSubdivision]);

  return (
    <>
      <Typography variant="h6" gutterBottom>
        Shipping address
      </Typography>
      <FormProvider {...methods}>
        <form
          onSubmit={methods.handleSubmit((data) =>
            next({
              ...data,
              shippingCountry,
              shippingSubdivision,
              shippingOption,
            })
          )}
        >
          <Grid container spacing={3}>
            <Grid item xs={12} sm={6}>
              <FormInput required name="firstName" label="First name" />{" "}
            </Grid>
            <Grid item xs={12} sm={6}>
              {" "}
              <FormInput required name="lastName" label="Last name" />
            </Grid>
            <Grid item xs={12} sm={6}>
              {" "}
              <FormInput required name="address1" label="Address line 1" />
            </Grid>
            <Grid item xs={12} sm={6}>
              {" "}
              <FormInput required name="email" label="Email" />
            </Grid>
            <Grid item xs={12} sm={6}>
              {" "}
              <FormInput required name="city" label="City" />
            </Grid>
            <Grid item xs={12} sm={6}>
              {" "}
              <FormInput required name="zip" label="Zip / Postal code" />
            </Grid>

            <Grid item xs={12} sm={6}>
              <InputLabel>Shipping Country</InputLabel>
              <Select
                value={shippingCountry}
                fullWidth
                onChange={(e) => setShippingCountry(e.target.value)}
              >
                {Object.entries(shippingCountries)
                  .map(([code, name]) => ({ id: code, label: name }))
                  .map((item) => (
                    <MenuItem key={item.id} value={item.id}>
                      {item.label}
                    </MenuItem>
                  ))}
              </Select>
            </Grid>
            <Grid item xs={12} sm={6}>
              <InputLabel>Shipping Subdivision</InputLabel>
              <Select
                value={shippingSubdivision}
                fullWidth
                onChange={(e) => setShippingSubdivision(e.target.value)}
              >
                {Object.entries(shippingSubdivisions)
                  .map(([code, name]) => ({ id: code, label: name }))
                  .map((item) => (
                    <MenuItem key={item.id} value={item.id}>
                      {item.label}
                    </MenuItem>
                  ))}
              </Select>
            </Grid>
            <Grid item xs={12} sm={6}>
              <InputLabel>Shipping Options</InputLabel>
              <Select
                value={shippingOption}
                fullWidth
                onChange={(e) => setShippingOption(e.target.value)}
              >
                {shippingOptions
                  .map((sO) => ({
                    id: sO.id,
                    label: `${sO.description} - (${sO.price.formatted_with_symbol})`,
                  }))
                  .map((item) => (
                    <MenuItem key={item.id} value={item.id}>
                      {item.label}
                    </MenuItem>
                  ))}
              </Select>
            </Grid>
          </Grid>
          <br />
          <div style={{ display: "flex", justifyContent: "space-between" }}>
            <Button component={Link} variant="outlined" to="/cart">
              Back to Cart
            </Button>
            <Button type="submit" variant="contained" color="primary">
              Next
            </Button>
          </div>
        </form>
      </FormProvider>
    </>
  );
};

export default AddressForm;

My payment form component:

import React from "react";
import { Typography, Button, Divider } from "@material-ui/core";
import {
  Elements,
  CardElement,
  ElementsConsumer,
} from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";

import Review from "./Review";

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY);

const PaymentForm = ({
  checkoutToken,
  nextStep,
  backStep,
  shippingData,
  onCaptureCheckout,
}) => {
  const handleSubmit = async (event, elements, stripe) => {
    event.preventDefault();

    if (!stripe || !elements) return;

    const cardElement = elements.getElement(CardElement);

    const { error, paymentMethod } = await stripe.createPaymentMethod({
      type: "card",
      card: cardElement,
    });

    if (error) {
      console.log("[error]", error);
    } else {
      const orderData = {
        line_items: checkoutToken.live.line_items,
        customer: {
          firstname: shippingData.firstName,
          lastname: shippingData.lastName,
          email: shippingData.email,
        },
        shipping: {
          name: "Primary",
          street: shippingData.address1,
          town_city: shippingData.city,
          county_state: shippingData.shippingSubdivision,
          postal_zip_code: shippingData.zip,
          country: shippingData.shippingCountry,
        },
        fulfillment: { shipping_method: shippingData.shippingOption },
        payment: {
          gateway: "stripe",
          stripe: {
            payment_method_id: paymentMethod.id,
          },
        },
      };

      onCaptureCheckout(checkoutToken.id, orderData);

      nextStep();
    }
  };

  return (
    <>
      <Review checkoutToken={checkoutToken} />
      <Divider />
      <Typography variant="h6" gutterBottom style={{ margin: "20px 0" }}>
        Payment method
      </Typography>
      <Elements stripe={stripePromise}>
        <ElementsConsumer>
          {({ elements, stripe }) => (
            <form onSubmit={(e) => handleSubmit(e, elements, stripe)}>
              <CardElement />
              <br /> <br />
              <div style={{ display: "flex", justifyContent: "space-between" }}>
                <Button variant="outlined" onClick={backStep}>
                  Back
                </Button>
                <Button
                  type="submit"
                  variant="contained"
                  disabled={!stripe}
                  color="primary"
                >
                  Pay {checkoutToken.live.subtotal.formatted_with_symbol}
                </Button>
              </div>
            </form>
          )}
        </ElementsConsumer>
      </Elements>
    </>
  );
};

export default PaymentForm;

And also my app js code:

import React, { useState, useEffect } from "react";
import { Products, Navbar, Cart, Checkout } from "./components";
import { commerce } from "./lib/commerce";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";

const App = () => {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState({});
  const [order, setOrder] = useState({});
  const [errorMessage, setErrorMessage] = useState("");

  const fetchProducts = async () => {
    const { data } = await commerce.products.list();

    setProducts(data);
  };

  const fetchCart = async () => {
    setCart(await commerce.cart.retrieve());
  };

  const handleAddToCart = async (productId, quantity) => {
    const { cart } = await commerce.cart.add(productId, quantity);

    setCart(cart);
  };

  const handleUpdateCartQty = async (productId, quantity) => {
    const { cart } = await commerce.cart.update(productId, { quantity });
    setCart(cart);
  };

  const handleRemoveFromCart = async (productId) => {
    const { cart } = await commerce.cart.remove(productId);
    setCart(cart);
  };

  const handleEmptyCart = async () => {
    const { cart } = await commerce.cart.empty();
    setCart(cart);
  };

  const refreshCart = async () => {
    const newCart = await commerce.cart.refresh();
    console.log(newCart);
    setCart(newCart);
  };

  const handleCaptureCheckout = async (checkoutTokenId, newOrder) => {
    try {
      const incomingOrder = await commerce.checkout.capture(
        checkoutTokenId,
        newOrder
      );

      setOrder(incomingOrder);

      refreshCart();
    } catch (error) {
      setErrorMessage(error.data.error.message);
    }
  };

  useEffect(() => {
    fetchProducts();
    fetchCart();
  }, []);

  return (
    <Router>
      <div>
        <Navbar totalItems={cart.total_items} />
        <Switch>
          <Route exact path="/">
            <Products products={products} onAddToCart={handleAddToCart} />
          </Route>

          <Route exact path="/cart">
            <Cart
              cart={cart}
              handleUpdateCartQty={handleUpdateCartQty}
              handleRemoveFromCart={handleRemoveFromCart}
              handleEmptyCart={handleEmptyCart}
            />
          </Route>
          <Route exact path="/checkout">
            <Checkout
              cart={cart}
              order={order}
              onCaptureCheckout={handleCaptureCheckout}
              error={errorMessage}
            />
          </Route>
        </Switch>
      </div>
    </Router>
  );
};

export default App;

I spoke to some people on the Commerce JS slack and they pointed out that these fields were ‘missing’ but I don’t understand what they mean by that. I’ve read that others have had this issue too (specifically people following this tutorial: ECommerce Web Shop - Build & Deploy an Amazing App | React.js, Commerce.js, Stripe - YouTube) so I’m not sure if it’s a problem with the code or the API.

I’m at a loss here so I’d really appreciate any help or suggestions.

Thanks

Hello! Welcome to the community :grin:!

I may be missing something, but where are you importing your PaymentForm?

Regardless, if you log the request to the payment API (or you check the request under the Network tab on the developer tools of your browser), is all the required data being sent to the API?

Hello,
I am having the exact same problem as the original poster, this is the response from the API
{
“status_code”: 422,
“error”: {
“message”: “The given data was invalid.”,
“type”: “unprocessable_entity”,
“errors”: {
“customer.email”: [
“The Email field is required when customer.id is not present.”
],
“shipping.street”: [
“The Shipping street address field is required.”
],
“shipping.town_city”: [
“The Shipping town/city field is required.”
]
}
},
“help”: {
“slack”: “http://slack.commercejs.com”,
“_comment”: “help & console error responses will not be returned when using production/live API keys.”
}
}
All other components are also identical to the OP. Did everything like the video
I am assuming it has something to do with not sending the data properly? because in the API log request the customer field is empty.
Any inputs would be appreciated, thank you!