Create a Checkout with React and Three.js: Part 2

by Craig Gant on September 21, 2020
View on GitHUb

This guide shows you how to set up a checkout experience using Commerce.js, React, Three.js, and React-Three-Fiber.

This is part 2 of this guide, you can read part 1 here

In the previous guide we:

  • Uploaded products
  • Setup file structure
  • Added CSS
  • Set useState() hooks for the checkout
  • Handled user inputs
  • Wrote helper functions for the checkout
  • Created a reusable card

Let's get right back into it!

8. The Animation

Next, turn your attention to the animation folder. Set up Animation.js the same way you set up three.js animations in the previous create a cart guide. Start by declaring the canvas element, camera, and lighting.

//Animation.js

import React from "react";
import { Canvas } from "react-three-fiber";
import * as THREE from "three";
import "../styles.scss";

export default function Animation(props) {
  return (
    <div>
      <Canvas
        className={"cardAnimation"}
        camera={{ position: [0, 0, 25] }}
        onCreated={({ gl }) => {
          gl.shadowMap.enabled = true;
          gl.shadowMap.type = THREE.PCFSoftShadowMap;
        }}
      >
        {/* Lighting */}
        <ambientLight intensity={0.35} />
        <spotLight
          intensity={1}
          position={[10, 60, 40]}
          penumbra={1}
          shadow-mapSize-width={2048}
          shadow-mapSize-height={2048}
          castShadow
        />
      </Canvas>
    </div>
  );
}

Next, navigate to Scene.js and import the model that you will use as the main object in this scene. This guide uses a credit card model from sketchfab. Just as you did in the last guide, use gltfjsx to create editable, declarative models (just a reminder: you will need to add the gltf file and textures folder to your public folder). By changing the values of a mesh's material property, you can make the credit card look however you wish. This guide will be using .png files to replicate different credit cards. If needed, you can find the files this guide uses here.

// Scene.js

import * as THREE from "three";
import React, { useRef, useMemo } from "react";
import { useLoader } from "react-three-fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

export default function Model(props) {
  // This renders the model for the credit card, and also loads the appropriate card background as user inputs change

  // Loads background image
  const texture = useMemo(
    () => new THREE.TextureLoader().load(props.cardType),
    [props.cardType]
  );

  const group = useRef();
  // loads the card model
  const { nodes, materials } = useLoader(GLTFLoader, "./card/scene.gltf");
  // declares a new material and maps the background image to that material
  const material = new THREE.MeshPhysicalMaterial({ map: texture });

  return (
    <group ref={group} {...props} dispose={null}>
      <group rotation={[-Math.PI / 2, 0, 0]} position={[-6.1, -9, 6]}>
        {/* Back of the card */}
        <mesh
          material={material}
          geometry={nodes.mesh_0.geometry}
          castShadow
          metalness={8}
        />
        {/* Outer rim of card */}
        <mesh
          material={materials.Card3initialSha}
          geometry={nodes.mesh_1.geometry}
          castShadow
        />
        {/* Front of Card */}
        <mesh
          material={material}
          geometry={nodes.mesh_2.geometry}
          castShadow
          metalness={8}
        />
      </group>
    </group>
  );
}

To make your credit card come alive even more, you will need to handle text within your animation. To do this, open CardText.js. Import Text from drei and declare the following functional component. Note that the component takes in rotation, position, font size, and text content via props.

// CardText.js

import React from "react";
import { Text } from "drei";

function CardText(props) {
  // This element handles text elements that can then be rendered within our animation
  return (
    <Text
      rotation={props.rotation}
      position={props.position}
      color={"black"}
      fontSize={props.fontSize}
      font="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap"
      anchorX="left"
      anchorY="middle"
    >
      {props.text}
    </Text>
  );
}
export default CardText;

You now have all the elements you need to make up the card object in your animation. To put everything together, turn your attention to Card.js. Take advantage of React's useRef() hook to make your card rotate on the page, and wrap all of your elements in a <group>. Grouping ensures that the card, text, and magnetic strip will act as a single unit and not disparate elements.

It's also important to wrap the group in a <Suspense> element. By doing this, you ensure that the page will render and display a fallback, even if something happens to your animation. For the sake of this guide, import Suspense from react and use fallback={null}.

//Card.js

import React, { useRef, Suspense } from "react";
import CardText from "./CardText";
import { useFrame } from "react-three-fiber";
import * as THREE from "three";
import Model from "./Scene";

function Card(props) {
  // This element is the credit card animation
  const creditCard = useRef();
  // Card rotates automatically along the y axis
  useFrame(() => (creditCard.current.rotation.y += 0.003));

  return (
    <Suspense fallback={null}>
      <group ref={creditCard}>
        {/* Card itself */}
        <Model
          cardType={props.cardType}
          number={props.cardNum}
          name={props.cardName}
          expiry={props.expDate}
          cvv={props.cvv}
        />
        {/* Magnetic Strip on back of card */}
        <mesh receiveShadow position={[0.19, 3.1, 0.73]}>
          <planeBufferGeometry attach="geometry" args={[26.8, 3]} />
          <meshStandardMaterial
            attach="material"
            color="#0f0f0f"
            opacity={0.3}
            side={THREE.DoubleSide}
            metalness={6}
          />
        </mesh>
        {/* Card Number */}
        <CardText
          position={[-11, -0.4, 0.89]}
          text={props.number}
          fontSize={2}
        />
        {/* Customer Name */}
        <CardText position={[-11, -6, 0.89]} text={props.name} fontSize={1.8} />
        {/* Valid Thru */}
        <CardText position={[5, -4, 0.89]} text={"Valid Thru"} fontSize={1} />
        {/* Expiration Date */}
        <CardText position={[5, -6, 0.89]} text={props.expiry} fontSize={1.8} />
        {/* Cvv */}
        <CardText
          rotation={[0, Math.PI, 0]}
          position={[-5, 0, 0.73]}
          text={props.cvv}
          fontSize={1.9}
        />
      </group>
    </Suspense>
  );
}

export default Card;

To make your card appear in the scene, navigate back to Animation.js, import Card, and add the following code underneath the <spotLight /> element:

//Animation.js

<Card
  cardType={props.cardType}
  number={props.cardNum}
  name={props.cardName}
  expiry={props.expDate}
  cvv={props.cvv}
/>

Now is a good time to add controls to your animation. In Controls.js create the following functional component.

// Controls.js

import React, { useRef } from "react";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { extend, useThree, useFrame } from "react-three-fiber";
import "../styles.scss";

extend({ OrbitControls });

export default function Controls() {
  // Imports controls for the animation
  const orbitRef = useRef();
  const { camera, gl } = useThree();

  useFrame(() => {
    orbitRef.current.update();
  });

  return (
    <orbitControls
      // disables ability to pan and zoom the camera
      enablePan={false}
      enableZoom={false}
      ref={orbitRef}
      args={[camera, gl.domElement]}
    />
  );
}

Now add <Controls /> to Animation.js.

To give the animation some depth, you can create a box around the card so that it looks like it is rotating within a small room. Start by creating a plane in BackDrop.js. Make sure to declare THREE.DoubleSide in your mesh. This declaration ensures that the plane is visible from both sides, which will be very important when you reposition these planes into a box.

//BackDrop.js

import React from "react";
import * as THREE from "three";

function BackDrop({ position, rotation, opacity }) {
  // This element is one wall (used to make up the sides of the skybox background)
  return (
    <mesh receiveShadow position={position} rotation={rotation}>
      <planeBufferGeometry attach="geometry" args={[101, 101]} />
      <meshStandardMaterial
        side={THREE.DoubleSide}
        attach="material"
        color="#eeeeee"
        opacity={opacity}
      />
    </mesh>
  );
}

export default BackDrop;

Now, navigate to Skybox.js and return a fragment with six instances of BackDrop, positioned and rotated to make a box.

// Skybox.js

import React from "react";
import BackDrop from "./BackDrop";

function Skybox() {
  return (
    <>
      <BackDrop
        rotation={[-Math.PI / 2, 0, 0]}
        position={[0, -30, 0]}
        opacity={0.3}
      />
      <BackDrop
        rotation={[-Math.PI / 2, 0, 0]}
        position={[0, 30, 0]}
        opacity={0.3}
      />
      <BackDrop rotation={[0, 0, 0]} position={[0, -1, -50]} opacity={0.35} />
      <BackDrop
        rotation={[0, Math.PI / 2, 0]}
        position={[-50, -1, 0]}
        opacity={0.35}
      />
      <BackDrop
        rotation={[0, Math.PI / 2, 0]}
        position={[50, -1, 0]}
        opacity={0.35}
      />
      <BackDrop rotation={[0, 0, 0]} position={[0, -1, 50]} opacity={0.35} />
    </>
  );
}

export default Skybox;

You can now add the <Skybox /> element to your scene.

All together, Animation.js should look something like this:

Click to see the finished Animation.js file.

// Animation.js

import React from "react";
import { Canvas } from "react-three-fiber";
import * as THREE from "three";
import Controls from "./Controls";
import Card from "./Card";
import Skybox from "./Skybox";
import "../styles.scss";

export default function Animation(props) {
  return (
    <div>
      <Canvas
        className={"cardAnimation"}
        camera={{ position: [0, 0, 25] }}
        onCreated={({ gl }) => {
          gl.shadowMap.enabled = true;
          gl.shadowMap.type = THREE.PCFSoftShadowMap;
        }}
      >
        <Controls />
        {/* Lighting */}
        <ambientLight intensity={0.35} />
        <spotLight
          intensity={1}
          position={[10, 60, 40]}
          penumbra={1}
          shadow-mapSize-width={2048}
          shadow-mapSize-height={2048}
          castShadow
        />
        {/* Animated Card */}
        <Card
          cardType={props.cardType}
          number={props.cardNum}
          name={props.cardName}
          expiry={props.expDate}
          cvv={props.cvv}
        />
        {/* Walls surrounding card */}
        <Skybox />
      </Canvas>
    </div>
  );
}

9. Create a form with animation

Now that you have built out Animation.js, you can combine it with an input form. Start by copying the code from FormCard.js into FormCardWithAnimation.js and then add <Animation /> to the <Form />. (Be sure to pass the needed information into <Animation /> via props.) You can also add some instructions for the user, so they know how to best interact with your card.

All told, your file should look something like this:

Click to see `FormCardWithAnimation.js`
//FormCardWithAnimation.js

import React, { useState } from "react";
import { Row, Form, Card, Col } from "react-bootstrap";
import { useSpring, animated } from "react-spring";
import FormElement from "./FormElement";
import Animation from "../Animation/Animation";

function FormCardWithAnimation(props) {
  // Animation value
  const [hovered, setHovered] = useState(false);
  // Sets animation for checkout box when hovered
  const hovering = useSpring({
    transform: hovered
      ? "translate3d(0px,0,0) scale(1.05) rotateX(0deg)"
      : "translate3d(0px,0,0) scale(1) rotateX(0deg)",
  });

  return (
    <animated.div
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
      style={hovering}
    >
      <Row>
        <Card as={Col} sm={12} className="rounded infoCard">
          <h1 className="text-center mt-4">{props.title}</h1>
          <p className="text-center" style={{ color: "#eae6e5" }}>
            Interact with the card and watch it respond as you enter numbers
            beginning with 4, 5, 6, 35, and 37.
            <span>
              <br></br>
            </span>
            Use 4242 4242 4242 4242 to test payment.
          </p>
          <Form onSubmit={props.handleSubmit} className="p-4 ">
            {/* Renders a canvas and animation to form */}
            <Animation
              cardNum={props.cardNum}
              cardName={props.cardName}
              expDate={props.expDate}
              cvv={props.cvv}
              cardType={props.cardType}
            />

            <Form.Row className="mt-3">
              {/* Iterates through the fields stipulated and renders an input for each */}
              {props.formDetails.map((field) => (
                <FormElement
                  key={field.controlId}
                  controlId={field.controlId}
                  smallColSize={field.smallColSize}
                  formLabel={field.formLabel}
                  placeholder={field.placeholder}
                  name={field.name}
                  handleChange={props.handleChange}
                  value={field.value}
                />
              ))}
            </Form.Row>
          </Form>
        </Card>
      </Row>
    </animated.div>
  );
}

export default FormCardWithAnimation;

Since you created CardFormDetails.js earlier, simply import FormCardWithAnimation in App.js and place the following code under <FormCard />.

//App.js

<FormCardWithAnimation
  formDetails={CardFormDetails(cardNum, cardName, expDate, cvv)}
  handleChange={handleCardChange}
  handleSubmit={handleSubmit}
  title={"Payment"}
  cardNum={cardNum}
  cardName={cardName}
  expDate={expDate}
  cvv={cvv}
  cardType={cardType}
/>

10. Making your animated card dynamic

To make your animation change as the user inputs their information, you will need one additional helper function. In helperFunctions.js, import the .png files you will be mapping to your animated card (the files used in this guide can be found here). Then add the following function, which will take in a user's card number and return a string representing the user's card type.

// helperFunctions.js

export function findCardType(number) {
  const firstNumber = number.toString().charAt(0);
  const secondNumber = number.toString().slice(1, 2);
  let cardType;

  switch (firstNumber) {
    case "3":
      !secondNumber
        ? (cardType = Default)
        : secondNumber === "4" || secondNumber === "7"
        ? (cardType = Amex)
        : secondNumber === "5"
        ? (cardType = Jcb)
        : (cardType = Diners);
      break;
    case "4":
      cardType = Visa;
      break;
    case "5":
      cardType = Mastercard;
      break;
    case "6":
      cardType = Discover;
      break;
    default:
      cardType = Default;
      break;
  }
  return cardType;
}

Now return to App.js and import the function.

11. Alerts and notifications

The last thing you will need for a fully functional checkout experience is a way to provide feedback to your customer.

Navigate to Spinner.js and create a Bootstrap <Spinner>.

// Spinner.js
import React from "react";
import { Spinner } from "react-bootstrap";

function SubmissionSpinner({ visible }) {
  // The spinner to be shown only after a user clicks "complete order".
  const display = visible ? { zIndex: 100 } : { display: "none" };
  return (
    <Spinner
      animation="border"
      role="status"
      style={display}
      id="submissionSpinner"
    >
      <span className="sr-only">Please wait...</span>
    </Spinner>
  );
}

export default SubmissionSpinner;

Return to App.js, import your new component, and then the following code directly underneath the <Container> element.

//App.js

<SubmissionSpinner visible={spinnerVisible} />

Another important way to provide feedback to your user is through the use of popup alerts. To make one, begin by importing Alert from "react-bootstrap". Then, directly underneath the <Container> element in App.js, use the setShowSuccess and setShowFail hooks you set up earlier to make your alerts render conditionally.

// App.js

{/* success popup  */}
{showSuccess ? (
  <Alert
    className="popup"
    variant={"success"}
    onClick={() => setShowSuccess(false)}
    dismissible
  >
    Success! Your order has been received. Thanks for shopping with us!
  </Alert>
) : (
  <div></div>
)}
{/* error popup */}
{showFail ? (
  <Alert
    className="popup"
    variant={"warning"}
    onClick={() => setShowFail(false) && setValidationInfo(null)}
    dismissible
  >
    There was a problem with your {validationInfo}. Please ensure your
    information was entered correctly and try again.
  </Alert>
) : (
  <div></div>
)}

12. That's it!

You should have an application that uses Commerce.js to manage a user's checkout experience and a Three.js credit card that reflects a user's inputs as they type.

View the live demo

Built with:

Want to help create guides?

Get in touch with the Commerce.js team and get paid to create projects for the community.

#checkout #animation #react #threejs

About the author, Craig Gant

Craig is a web developer and musician who lives in central Florida. He loves all things JavaScript. When he’s not coding, he enjoys exploring theme parks, listening to jazz, and reading.