V-FOR-VEND3TTA / commerce-js-site

A React frontend and Commerce.js backend ecommerce website
0 stars 0 forks source link

React.js checkout #7

Open V-FOR-VEND3TTA opened 12 months ago

V-FOR-VEND3TTA commented 12 months ago
this.state = {
  checkoutToken: {},
  // Customer details
  firstName: 'Jane',
  lastName: 'Doe',
  email: 'janedoe@email.com',
  // Shipping details
  shippingName: 'Jane Doe',
  shippingStreet: '123 Fake St',
  shippingCity: 'San Francisco',
  shippingStateProvince: 'CA',
  shippingPostalZipCode: '94107',
  shippingCountry: 'US',
  // Payment details
  cardNum: '4242 4242 4242 4242',
  expMonth: '11',
  expYear: '2023',
  ccv: '123',
  billingPostalZipcode: '94107',
  // Shipping and fulfillment data
  shippingCountries: {},
  shippingSubdivisions: {},
  shippingOptions: [],
  shippingOption: '',
}

With a created function fetchShippingCountries(), use commerce.services.localeListShipppingCountries() at GET v1/services/locale/{checkout_token_id}/countries to fetch and list all countries in the select options in the form.

/**
 * Fetches a list of countries available to ship to checkout token
 * https://commercejs.com/docs/sdk/checkout#list-available-shipping-countries
 *
 * @param {string} checkoutTokenId
 */
fetchShippingCountries(checkoutTokenId) {
  commerce.services.localeListShippingCountries(checkoutTokenId).then((countries) => {
    this.setState({ 
      shippingCountries: countries.countries,
    })
  }).catch((error) => {
    console.log('There was an error fetching a list of shipping countries', error);
  });
}

The response will be stored in the shippingCountries object you initialized earlier in the constructor. You will then be able to use this countries object to iterate and display a list of countries in a select element, which you will be adding later. The fetchSubdivisions() function below will walk through the same pattern as well.

A country code argument is required to make a request with commerce.services.localeListSubdivisions() to GET v1/services/locale/{country_code}/subdivisions to get a list of all subdivisions for that particular country.

/**
 * Fetches the subdivisions (provinces/states) in a country which
 * can be shipped to for the current checkout
 * https://commercejs.com/docs/sdk/checkout#list-subdivisions
 *
 * @param {string} countryCode
 */
fetchSubdivisions(countryCode) {
  commerce.services.localeListSubdivisions(countryCode).then((subdivisions) => {
      this.setState({
        shippingSubdivisions: subdivisions.subdivisions,
      })
  }).catch((error) => {
      console.log('There was an error fetching the subdivisions', error);
  });
},

With a successful request, the response will be stored in the this.state.shippingSubdivions array and will be used to iterate and output a select element in your template later on.

For your next checkout helper function, fetch the current shipping options available in your merchant account. This function will fetch all the shipping options that were registered in the Chec Dashboard and are applicable to the products in your cart using the commerce.checkout.getShippingOptions() method. This function takes in two required parameters - the checkoutTokenId, the country code for the provide country in our data, and the region is optional.

/**
 * Fetches the available shipping methods for the current checkout
 * https://commercejs.com/docs/sdk/checkout#get-shipping-methods
 *
 * @param {string} checkoutTokenId
 * @param {string} country
 * @param {string} stateProvince
 */
fetchShippingOptions(checkoutTokenId, country, stateProvince = null) {
  commerce.checkout.getShippingOptions(checkoutTokenId,
    { 
      country: country,
      region: stateProvince
    }).then((options) => {
      const shippingOption = options[0] || null;
      this.setState({
        shippingOptions: options,
        shippingOption: shippingOption,
      })
    }).catch((error) => {
      console.log('There was an error fetching the shipping methods', error);
  });
}

When the promise resolves, the response will be stored into this.state.shippingOptions which you can then use to render a list of shipping options in your template. Note that the shippingOption is set to the first index option in the array to have an option in the dropdown be displayed.

Alright, that wraps up all the checkout helper functions you'll want to create for the checkout page. Now it's time to execute and hook up the responses to your render function!

Start by chaining and calling the fetchShippingCountries() in the generateCheckoutToken function for ease of execution.

/**
 *  Generates a checkout token
 *  https://commercejs.com/docs/sdk/checkout#generate-token
 */
generateCheckoutToken() {
  const { cart } = this.props;
  if (cart.line_items.length) {
    return commerce.checkout.generateToken(cart.id, { type: 'cart' })
      .then((token) => this.setState({ checkoutToken: token }))
      .then(() => this.fetchShippingCountries(this.state.checkoutToken.id))
      .catch((error) => {
        console.log('There was an error in generating a token', error);
      });
  }
}

Next, call generateCheckoutToken() in componentDidMount().

componentDidMount() {
  this.generateCheckoutToken();
}

In the same vein, you'll need to call the fetchShippingOptions() function to display the list of shipping options available. Add this.fetchShippingOptions(this.state.checkoutToken.id, this.state.shippingCountry) into a lifecycle hook that checks whether the shipping country state has changed.

componentDidUpdate(prevProps, prevState) {
  if (this.state.form.shipping.country !== prevState.form.shipping.country) {
    this.fetchShippingOptions(this.state.checkoutToken.id, this.state.shippingCountry);
  }
}

You will now need to bind all the data responses to the shipping form fields. In the shipping section of the JSX render you created earlier on, place all the markup underneath the Postal/Zip code input field:

<label className="checkout__label" htmlFor="shippingCountry">Country</label>
<select
  value={this.state.shippingCountry}
  name="shippingCountry"
  className="checkout__select"
>
  <option disabled>Country</option>
  {
    Object.keys(shippingCountries).map((index) => {
      return (
        <option value={index} key={index}>{shippingCountries[index]}</option>
      )
    })
  };
</select>

<label className="checkout__label" htmlFor="shippingStateProvince">State/province</label>
<select 
  value={this.state.shippingStateProvince}
name="shippingStateProvince"
  className="checkout__select"
>
  <option className="checkout__option" disabled>State/province</option>
  {
    Object.keys(shippingSubdivisions).map((index) => {
      return (
        <option value={index} key={index}>{shippingSubdivisions[index]}</option>
      );
    })
  };
</select>

<label className="checkout__label" htmlFor="shippingOption">Shipping method</label>
<select
  value={this.state.shippingOption.id}
name="shippingOption"
  className="checkout__select"
>
  <option className="checkout__select-option" disabled>Select a shipping method</option>
  {
    shippingOptions.map((method, index) => {
      return (
        <option className="checkout__select-option" value={method.id} key={index}>{`${method.description} - $${method.price.formatted_with_code}` }</option>
      );
    })
  };
</select>

The three fields you just added:

Binds this.state.shippingCountry as the selected country and loops through the shippingCountries array to render as options Binds this.state.shippingStateProvince as the selected state/province and iterates through the shippingSubivisions object to display the available list of countries Binds this.state.shippingOption and loops through the shippingOptions array to render as options in the Shipping method field. Currently the prepopulated data defined in the state earlier will be bound to each of the input but changes to the input fields have not been handled yet. An onChange handler will need to be added to handle necessary form field value changes. Create a handler function handleFormChanges() to update the state with changed input values. The handler will also need to be bound in the constructor.

handleFormChanges(e) {
  this.setState({
    [e.target.name]: e.target.value,
  });
};
this.handleFormChanges = this.handleFormChanges.bind(this)

The value prop here is setting the value to the latest state so that you can see the inputted value as it is being typed. Next, attach the handler to the onChange attribute in each of the input fields as well as the shipping state/province and shipping options select fields.

<form className="checkout__form" onChange={this.handleFormChanges}>
  <h4 className="checkout__subheading">Customer information</h4>

  <label className="checkout__label" htmlFor="firstName">First name</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.firstName} name="firstName" placeholder="Enter your first name" required />

  <label className="checkout__label" htmlFor="lastName">Last name</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.lastName}name="lastName" placeholder="Enter your last name" required />

  <label className="checkout__label" htmlFor="email">Email</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.email} name="email" placeholder="Enter your email" required />

  <h4 className="checkout__subheading">Shipping details</h4>

  <label className="checkout__label" htmlFor="shippingName">Full name</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.shippingName} name="shippingName" placeholder="Enter your shipping full name" required />

<label className="checkout__label" htmlFor="shippingStreet">Street address</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.shippingStreet} name="shippingStreet" placeholder="Enter your street address" required />

  <label className="checkout__label" htmlFor="shippingCity">City</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.shippingCity} name="shippingCity" placeholder="Enter your city" required />

  <label className="checkout__label" htmlFor="shippingPostalZipCode">Postal/Zip code</label>
  <input className="checkout__input" type="text" onChange={this.handleFormChanges} value={this.state.shippingPostalZipCode} name="shippingPostalZipCode" placeholder="Enter your postal/zip code" required />

 <label className="checkout__label" htmlFor="shippingCountry">Country</label>
  <select
    value={this.state.shippingCountry}
    name="shippingCountry"
    className="checkout__select"
  >
    <option disabled>Country</option>
    {
      Object.keys(shippingCountries).map((index) => {
        return (
          <option value={index} key={index}>{shippingCountries[index]}</option>
        );
      })
    };
  </select>

  <label className="checkout__label" htmlFor="shippingStateProvince">State/province</label>
  <select 
    value={this.state.shippingStateProvince}
    name="shippingStateProvince"
    onChange={this.handleFormChanges}
<label className="checkout__label" htmlFor="shippingCountry">Country</label>
  <select
    value={this.state.shippingCountry}
    name="shippingCountry"
    className="checkout__select"
  >
    <option disabled>Country</option>
    {
      Object.keys(shippingCountries).map((index) => {
        return (
          <option value={index} key={index}>{shippingCountries[index]}</option>
        );
      })
    };
  </select>

  <label className="checkout__label" htmlFor="shippingStateProvince">State/province</label>
  <select 
    value={this.state.shippingStateProvince}
    name="shippingStateProvince"
    onChange={this.handleFormChanges}
className="checkout__select"
  >
    <option className="checkout__option" disabled>State/province</option>
    {
      Object.keys(shippingSubdivisions).map((index) => {
        return (
          <option value={index} key={index}>{shippingSubdivisions[index]}</option>
        );
      })
    };
  </select>

  <label className="checkout__label" htmlFor="shippingOption">Shipping method</label>
  <select
    value={this.state.shippingOption.id}
    name="shippingOption"
    onChange={this.handleFormChanges}
    className="checkout__select"
  >
<option className="checkout__select-option" disabled>Select a shipping method</option>
    {
      shippingOptions.map((method, index) => {
        return (
          <option className="checkout__select-option" value={method.id} key={index}>{`${method.description} - $${method.price.formatted_with_code}` }</option>
        );
      })
    };
  </select>

  <h4 className="checkout__subheading">Payment information</h4>

  <label className="checkout__label" htmlFor="cardNum">Credit card number</label>
  <input className="checkout__input" type="text" name="cardNum" onChange={this.handleFormChanges} value={this.state.cardNum} placeholder="Enter your card number" />

  <label className="checkout__label" htmlFor="expMonth">Expiry month</label>
  <input className="checkout__input" type="text" name="expMonth" onChange={this.handleFormChanges} value={this.state.expMonth} placeholder="Card expiry month" />

  <label className="checkout__label" htmlFor="expYear">Expiry year</label>

<input className="checkout__input" type="text" name="expYear" onChange={this.handleFormChanges} value={this.state.expYear} placeholder="Card expiry year" />

  <label className="checkout__label" htmlFor="ccv">CCV</label>
  <input className="checkout__input" type="text" name="ccv" onChange={this.handleFormChanges} value={this.state.ccv} placeholder="CCV (3 digits)" />

  <button className="checkout__btn-confirm">Confirm order</button>
</form>

For data such as the shipping country and shipping subdivisions, it is necessary to handle the option changes separately. You will want to re-fetch and populate the select options with a new set of subdivisions according to the country selected. Since only one zone is enabled in the demo account, create two handlers to handle the shipping country option change as an example and bind the handler in the constructor. The shipping zones are also enabled at the subdivisions level, so you also want to handle changes to the subdivisions field.

handleShippingCountryChange(e) {
  const currentValue = e.target.value;
  this.fetchSubdivisions(currentValue);
};

]handleSubdivisionChange(e) {
      const currentValue = e.target.value;
      this.fetchShippingOptions(this.state.checkoutToken.id,           this.state.shippingCountry, currentValue)
}

Now update your shipping country select field by calling the handler in an onChange attribute.

<select
  value={this.state.shippingCountry}
  name="shippingCountry"
  className="checkout__select"
>
  <option disabled>Country</option>
  {
    Object.keys(shippingCountries).map((index) => {
      return (
        <option value={index} key={index}>{shippingCountries[index]}</option>
      );
    })
  };
</select>

Once all the data is bound to the field you are then able to collect the necessary data to convert the checkout into an order object.

Before creating a function to capture your checkout, you'll need to structure or 'sanitize' your line items to send in the checkout capture payload exactly how the request expects it. Write a function called sanitizedLineItems() to sanitize your line items.

sanitizedLineItems(lineItems) {
  return lineItems.reduce((data, lineItem) => {
    const item = data;
    let variantData = null;
    if (lineItem.selected_options.length) {
      variantData = {
        [lineItem.selected_options[0].group_id]: lineItem.selected_options[0].option_id,
      };
    }
    item[lineItem.id] = {
      quantity: lineItem.quantity,
      variants: variantData,
    };
  return item;
  }, {});
};

Now, create your handleCaptureCheckout() handler function and structure your returned data. Have a look at the expected structure here to send an order request. Note that in the line_items property value, you'll use the santizedLineItems function to pass in cart.line_items.

handleCaptureCheckout(e) {
  e.preventDefault();
  const orderData = {
    line_items: this.sanitizedLineItems(cart.line_items),
    customer: {
      firstname: this.state.firstName,
      lastname: this.state.lastName,
      email: this.state.email,
    },
    shipping: {
      name: this.state.shippingName,
      street: this.state.shippingStreet,
      town_city: this.state.shippingCity,
      county_state: this.state.shippingStateProvince,
      postal_zip_code: this.state.shippingPostalZipCode,
      country: this.state.shippingCountry,
    },
    fulfillment: {
      shipping_method: this.state.shippingOption.id
    },
    payment: {
      gateway: "test_gateway",
      card: {
        number: this.state.cardNum,
        expiry_month: this.state.expMonth,
        expiry_year: this.state.expYear,
        cvc: this.state.ccv,
        postal_zip_code: this.state.billingPostalZipcode,
      },
    },
  };
  this.props.onCaptureCheckout(this.state.checkoutToken.id, orderData);
};

Follow the exact structure of the data you intend to send and attach a callback function onCaptureCheckout to the handler and pass the order data object along with the required checkoutToken.id.

You need a button to handle the clicking of order confirmation, let's add that right now as the last element before the closing tag:

<button onClick={this.handleCaptureCheckout} className="checkout__btn-confirm">Confirm order</button>

Go back to your App.js to initialize an order data as an empty object where you store your returned order object.

this.state = {
  merchant: {},
  products: [],
  cart: {},
  isCartVisible: false,
  order: {},
};

Before creating an event handler to deal with your order capture, use another Commerce.js method called commerce.checkout.refreshCart(). When you call this function, it will refresh the cart in your state/session when the order is confirmed.

/**
 * Refreshes to a new cart
 * https://commercejs.com/docs/sdk/cart#refresh-cart
 */
refreshCart() {
  commerce.cart.refresh().then((newCart) => {
    this.setState({ 
      cart: newCart,
    });
  }).catch((error) => {
    console.log('There was an error refreshing your cart', error);
  });
};

Now create a helper function which will capture your order with the method commerce.checkout.capture(). It takes in the checkoutTokenId and the newOrder parameters. Upon the promise resolution, refresh the cart, store the order into the this.order property, and lastly use the router to push to a confirmation page which will be created in the last step.

/**
 * Captures the checkout
 * https://commercejs.com/docs/sdk/checkout#capture-order
 *
 * @param {string} checkoutTokenId The ID of the checkout token
 * @param {object} newOrder The new order object data
 */
handleCaptureCheckout(checkoutTokenId, newOrder) {
  commerce.checkout.capture(checkoutTokenId, newOrder).then((order) => {
    // Save the order into state
    this.setState({
      order,
    });
    // Clear the cart
    this.refreshCart();
    // Send the user to the receipt 
    this.props.history.push('/confirmation');
    // Store the order in session storage so we can show it again if the
    // user refreshes the page!
    window.sessionStorage.setItem('order_receipt', JSON.stringify(order));   
  }).catch((error) => {
    console.log('There was an error confirming your order', error);
  });
};

Now make sure you update and bind the necessary props and event handlers to a component passing in the component in the render:

<Route
  path="/checkout"
  exact
  render={(props) => {
    return (
      <Checkout
        {...props}
        cart={cart}
        onCaptureCheckout={this.handleCaptureCheckout}
      />
    );
  }}
/>

Lastly, create a simple confirmation view to display a successful order page.

render() { return ( <> { this.renderOrderSummary() } </> ); }; }

export default Confirmation;


The JSX will render a message containing the customer's name and an order reference.

In your App.js again, attach your order prop to your <Route> <Checkout> component instance:

<Route path="/confirmation" exact render={(props) => { if (!this.state.order) { return props.history.push('/'); } return ( <Confirmation {...props} order={order} /> ) }} />



That's it!
You have now completed the full series of the Commerce.js and React demo store.