loading...
Cover image for Building an E-Commerce application using Java & React
Adyen

Building an E-Commerce application using Java & React

deepu105 profile image Deepu K Sasidharan ・12 min read

E-commerce applications are the backbone of today's online shopping world. In this post, we will see how to build an e-commerce application easily using Java, JHipster, Spring Boot, and React. Since we will be scaffolding the application, the post will focus on how to build a shopping cart and payment integration rather than how to build a Java web application.

Tools and technology we will use

We will use the below tools and technology to build this application:

  • JHipster: JHipster is a rapid application development platform. It can quickly create web applications and microservices with production-grade code. Head over to the installation instructions to set it up. JHipster can scaffold applications with a wide variety of languages, frameworks, and configurations. For this tutorial, we will stick with the following major options. You don't have to install anything for this, as JHipster will manage these for you.
    • Spring Framework: Spring is an application framework in Java that comes with all the bells and whistles required for enterprise-grade Java application development. It comes with Spring Boot which makes development faster and convenient. This lets us focus more on our business needs rather than spending time setting up technical integrations.
    • React: A popular JavaScript UI library that helps build modern scalable front ends. We will be writing React code using TypeScript. We will also be using a few other components like Redux and React Router from the React ecosystem.
    • Bootstrap: An UI framework for web applications with a variety of themes and customizations.
    • Gradle: Gradle is a Java build orchestration tool that provides a highly customizable and easy-to-use domain-specific language (DSL).
    • Webpack: A front-end build tool for modern web applications
  • Adyen Payments Platform: Adyen is one of the leading payment platforms for medium to large scale businesses. It provides a plethora of payment options and provides SDKs for easy integrations. And I also happen to work for Adyen πŸ˜„
  • Docker: A containerization technology, which we will use it to quickly run our database. Make sure you have Docker and Docker compose installed. If you can run a local MySQL setup, you won't need Docker.
  • Git: Distributed version control system for source code management. Make sure you have Git installed.

Prerequisite

To follow this tutorial effectively you would need to be familiar with at least the below tools and technology

  • Java
  • Spring Framework
  • React
  • Redux
  • Bootstrap

We have a sample application built to accompany this post. Each section here is points to a particular commit in the sample app to help give you a better picture of what is being changed.

Designing the entity model

Since we are going to scaffold our application, it is important to make sure that we have the correct entity model for the e-commerce application. We will use the JHipster Domain Language(JDL) to do this. Below is the JDL model for an e-commerce application:

/** Product sold by the Online store */
entity Product {
    name String required
    description String
    price BigDecimal required min(0)
    size Size required
    image ImageBlob
}

enum Size {
    S, M, L, XL, XXL
}

/** Product categories to group products */
entity ProductCategory {
    name String required
    description String
}

/** Additional details for users as we can't modify built-in user entity vis JDL */
entity CustomerDetails {
    gender Gender required
    phone String required
    addressLine1 String required
    addressLine2 String
    city String required
    country String required
}

enum Gender {
    MALE, FEMALE, OTHER
}

/** Shopping cart to hold users orders */
entity ShoppingCart {
    placedDate Instant required
    status OrderStatus required
    totalPrice BigDecimal required min(0)
    paymentMethod PaymentMethod required
}

enum OrderStatus {
    COMPLETED, PAID, PENDING, CANCELLED
}

enum PaymentMethod {
    CREDIT_CARD, IDEAL
}

/** Product order keeps track of orders */
entity ProductOrder {
    quantity Integer required min(0)
    totalPrice BigDecimal required min(0)
}

// Every user will have a customer detail
relationship OneToOne {
    CustomerDetails{user(login) required} to User
}

// Many product orders can be tracked back to a product
relationship ManyToOne {
    ProductOrder{product(name) required} to Product
}

relationship OneToMany {
    // Every customer can have many shopping carts
    CustomerDetails{cart} to ShoppingCart{customerDetails required},
    // Every shopping cart can have many product orders
    ShoppingCart{order} to ProductOrder{cart required},
    // Every product category can have many products
    ProductCategory{product} to Product{productCategory(name) required}
}

service * with serviceClass
paginate Product, CustomerDetails, ProductCategory with pagination

The User entity is built-in from JHipster and hence we don't have to define it in JDL. However, we can define their relationships. Here is a UML visualization of the same:

Entity model for e-commerce

Head over to JDL Studio if you want to visualize the model and make any changes.

Next, create a new folder and save the above to a file named app.jdl within that folder.

Scaffolding the application

Now that we have our model in place, we can go ahead and scaffold a Java web application using JHipster. First, let's define our application. Add the below snippet to the file (app.jdl) we created earlier.

application {
  config {
    baseName store
    packageName com.adyen.demo.store
    authenticationType jwt
    prodDatabaseType mysql
    buildTool gradle
    clientFramework react
    useSass true
    enableTranslation false
  }
  entities *
}

We just defined an application named store that uses JSON Web Token (JWT) as the authentication mechanism, MySQL as the production database, Gradle as the build tool and React as the client-side framework. You can see all the options supported by JHipster here. We also defined that the application uses all the entities we defined with entities *.

Now, let's invoke JHipster to scaffold the application. Run the below command inside the folder where we created app.jdl:

jhipster import-jdl app.jdl

This will create our application, install all necessary dependencies, and initialize & commit everything to Git. Make sure you have Git installed on your system.

Let's check out the application. Run the below command to run the application in development mode:

./gradlew

After running the application, visit https://localhost:8080 and use the default users mentioned on the home page to log in and explore the application. You can find the commit in the sample application.

You can also run the generated unit and integration tests with this command:

./gradlew npm_test test integrationTest

So far, the generated application doesn't have any specific business logic or custom screens. It is just a CRUD application for the model we defined. If you are familiar with Spring Framework and React you should be able to navigate the source code created easily. The Spring/React application created by JHipster is not the focus of this post, and for that I recommend you head over to documentation provided by JHipster, Spring, and React.

Building a products landing page

Now that our application and all the CRUD APIs are ready, let us build a product landing page that lists all the products offered by the store.

We will convert src/main/webapp/app/modules/home/home.tsx to be our product landing page. This involves updating the JSX to show the products list and using the product redux reducer to fetch the data from product API. Here is the complete diff for home.tsx and here is the entire changelog for this step.

Product listing home page

Start the application client-side in dev mode to speed up development. Keep the application running in a terminal using ./gradlew if it not already running from the previous step. In a new terminal, run npm start and it will start a development server for the client-side, which proxies API calls to the backend and open up a new browser window pointing to https://localhost:9000.

At this point, the front-end and back-end are running in development mode with hot reload functionality. This means the entire application will automatically reload when we make any changes (the browser will reload as well). For backend changes, the reload will happen when you compile using your IDE or by running ./gradlew compileJava.

Update home.tsx according to the changelog and see the changes reflected on the home page.

Building the shopping cart

Now let us build a persistent shopping cart page, where we can list all the items added to the cart by the user. The user can also start checkout from this page. The shopping cart will hold the items added until the payment is complete even if the user logs out or uses the application in a different machine as the state is persisted automatically using the generated CRUD API:

Shopping cart

For this feature, we also add/update the below on the server-side:

These updates are quite straightforward due to the framework provided by JHipster and Spring.

On the client-side, we will update:

The shopping cart React page uses the below snippet. Note that the listing content is quite similar to the product listing page.

//import ...;

export interface ICartProp extends StateProps, DispatchProps {}

export const Cart = (props: ICartProp) => {
  useEffect(() => {
    props.getEntityForCurrentUser();
  }, []);

  const remove = id => () => {
    props.removeOrder(id);
  };

  const { isAuthenticated, cart, loading } = props;

  return (
    <Row className="d-flex justify-content-center">
      <Col lg="9" md="12">
        {isAuthenticated ? (
          <>
            <h2>Your shopping cart</h2>
            <p className="lead">You have {cart?.orders?.length} items in your cart</p>
            {cart.orders && cart.orders.length > 0 ? (
              <>
                <div className="list-group">
                  {cart.orders.map((order, i) => (
                    <a className="list-group-item list-group-item-action flex-column align-items-start">
                      <div className="row">{/*... list content */}</div>
                    </a>
                  ))}
                </div>
                <div className="d-flex justify-content-between py-4">
                  <h3>
                    Total price: <TextFormat value={cart.totalPrice as any} type="number" format={'$ 0,0.00'} />
                  </h3>
                  <Button tag={Link} to={`/checkout`} color="primary" size="lg">
                    <FontAwesomeIcon icon="cart-arrow-down" /> <span className="d-none d-md-inline">Checkout</span>
                  </Button>
                </div>
              </>
            ) : (
              !loading && <div className="alert alert-warning">No items found</div>
            )}
          </>
        ) : (
          <div>
            <Alert color="warning">Not authorized. Please log in first</Alert>
          </div>
        )}
      </Col>
    </Row>
  );
};

const mapStateToProps = ({ authentication, shoppingCart }: IRootState) => ({
  isAuthenticated: authentication.isAuthenticated,
  cart: shoppingCart.entity,
  loading: shoppingCart.loading
});

const mapDispatchToProps = {
  getEntityForCurrentUser,
  removeOrder
};

type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;

export default connect(mapStateToProps, mapDispatchToProps)(Cart);

Here is the entire changelog for this feature. Make the changes to the application and see the changes reflected on the shopping cart page.

Please note that I also made some improvements to the fake data generated by JHipster in this commit and made improvements to product and cart pages in this commit. I also fixed the tests in this commit. Update your application according to these changelogs as well.

Payments integration

Now that our shopping cart is ready, we can integrate the Adyen checkout API to make payments. First, make sure you sign up for an Adyen test account. Follow this guide to get your API keys and Merchant Account. You will also need to generate an origin key per domain you use to collect payments. In our case for development use, we need to create an origin key for http://localhost:9000 and http://localhost:8080.

Payment page

We will use the Adyen Java API library to make API calls. We will add the dependency to our Gradle build.

implementation group: "com.adyen", name: "adyen-java-api-library", version: "5.0.0"

We also need to exclude the Adyen domain in the content security policy defined in src/main/java/com/adyen/demo/store/config/SecurityConfiguration.java.

We will create a new Spring REST controller that will use the Adyen Java library and make payment API calls for us. Here is the src/main/java/com/adyen/demo/store/web/rest/CheckoutResource.java class. Here is a method from this class.

@PostMapping("/checkout/payment-methods")
public ResponseEntity<PaymentMethodsResponse> paymentMethods() throws EntityNotFoundException, IOException, ApiException {
    PaymentMethodsRequest paymentMethodsRequest = new PaymentMethodsRequest();
    paymentMethodsRequest.setMerchantAccount(merchantAccount);
    paymentMethodsRequest.setCountryCode("NL");
    paymentMethodsRequest.setShopperLocale("nl-NL");
    paymentMethodsRequest.setChannel(PaymentMethodsRequest.ChannelEnum.WEB);

    Amount amount = getAmountFromCart();
    paymentMethodsRequest.setAmount(amount);
    log.debug("REST request to get Adyen payment methods {}", paymentMethodsRequest);
    PaymentMethodsResponse response = checkout.paymentMethods(paymentMethodsRequest);
    return ResponseEntity.ok()
        .body(response);
}

The controller ensures that all actions are done against the active shopping cart of the user logged into the session. This ensures that security issues like man-in-the-middle attacks and request spoofing do not happen. When payment is completed successfully, we close the active shopping cart, ensuring every user has only one active shopping cart at a time.

On the client-side, we will create a React page to show the payment options and payment result status, a redux reducer to talk to the new API endpoints. We will also download and add the Adyen client-side resources to our index.html file.

Here are the important bits of the checkout page since this is where we handle the Adyen javascript integration from within React.

//import ...;

export interface ICheckoutProp extends StateProps, DispatchProps {}

class CheckoutContainer extends React.Component<ICheckoutProp> {
  private paymentContainer = React.createRef<HTMLDivElement>();
  //...

  componentDidMount() {
    this.props.getEntityForCurrentUser();
    this.props.getAdyenConfig();
    this.props.getPaymentMethods();
  }

  componentDidUpdate(prevProps: ICheckoutProp) {
    const { paymentMethodsRes, config, paymentRes, paymentDetailsRes, errorMessage } = this.props;
    if (errorMessage && errorMessage !== prevProps.errorMessage) {
      window.location.href = `/status/error?reason=${errorMessage}`;
      return;
    }
    if (paymentMethodsRes && config && (paymentMethodsRes !== prevProps.paymentMethodsRes || config !== prevProps.config)) {
      this.checkout = new AdyenCheckout({
        ...config,
        paymentMethodsResponse: this.removeNilFields(paymentMethodsRes),
        onAdditionalDetails: this.onAdditionalDetails,
        onSubmit: this.onSubmit
      });
    }
    if (paymentRes && paymentRes !== prevProps.paymentRes) {
      this.processPaymentResponse(paymentRes);
    }
    if (paymentRes && paymentDetailsRes !== prevProps.paymentDetailsRes) {
      this.processPaymentResponse(paymentDetailsRes);
    }
  }

  removeNilFields = obj => {
    //...
  };

  processPaymentResponse = paymentRes => {
    if (paymentRes.action) {
      this.paymentComponent.handleAction(paymentRes.action);
    } else {
      //...
      window.location.href = `/checkout/status/${urlPart}?reason=${paymentRes.resultCode}&paymentType=unknown`;
    }
  };

  onSubmit = (state, component) => {
    if (state.isValid) {
      this.props.initiatePayment({
        ...state.data,
        origin: window.location.origin
      });
      this.paymentComponent = component;
    }
  };

  onAdditionalDetails = (state, component) => {
    this.props.submitAdditionalDetails(state.data);
    this.paymentComponent = component;
  };

  handlePaymentSelect = (type: string) => () => {
    this.checkout.create(type).mount(this.paymentContainer?.current);
  };

  render() {
    const { cart } = this.props;

    return (
      <Row className="d-flex justify-content-center" style={{ minHeight: '80vh' }}>
        <Col lg="9" md="12">
          <h2>Make payment</h2>
          <p className="lead">You are paying total of € {cart.totalPrice}</p>
          <Row className="pt-4">
            <Col md="4" className="d-flex flex-column">
              <label>
                <strong>Choose a payment type</strong>
              </label>
              <ButtonGroup vertical>
                <Button onClick={this.handlePaymentSelect('card')}>Credit Card</Button>
                <Button onClick={this.handlePaymentSelect('ideal')}>iDEAL</Button>
              </ButtonGroup>
            </Col>
            <Col md="8">
              <div ref={this.paymentContainer} className="payment"></div>
            </Col>
          </Row>
        </Col>
      </Row>
    );
  }
}

const mapStateToProps = ({ checkout, shoppingCart }: IRootState) => ({
  //...
});

const mapDispatchToProps = {
  //...
};
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;

export default connect(mapStateToProps, mapDispatchToProps)(CheckoutContainer);

Here is the entire changelog for this feature. Make the changes to the application accordingly and see the changes reflected on the shopping cart page. Make sure to set the following environment variables first.

export ADYEN_API_KEY=yourAdyenApiKey
export ADYEN_MERCHANT_ACCOUNT=yourAdyenMerchantAccount
export ADYEN_ORIGIN_KEY=yourAdyenOriginKeyForCorrectDomain

Running the app in production

Now that we have made all the required changes, let us compile and run our app in production mode.

First, let us run the generated unit and integration tests to ensure we haven't broken anything:

./gradlew npm_test test integrationTest

Now, let's start a MySQL database as our application uses an in-memory H2 database for development and MySQL for production, this makes development easier. We will be using Docker compose to run the DB. You can also manually run a MySQL DB if you prefer.

docker-compose -f src/main/docker/mysql.yml up -d

The above command will start up MySQL DB from the included Docker compose file. Now, run the below command to run the application in production mode:

./gradlew -Pprod

You can also package the application using the command ./gradlew -Pprod clean bootJar and then run the JAR using java -jar build/libs/*.jar

Now, visit https://localhost:8080 and use the default users mentioned on the home page to log in and explore the application. You can use the test cards from Adyen to simulate payments

Conclusion

That's it. We have successfully built an e-commerce application complete with a product checkout and payment flow that can accept multiple forms of payment. I highly recommend that you check out the sample application to get a better context of what has been built.

The example application also has a user registration flow that you can checkout

I hope this helps anyone trying to build shopping carts and payment flows for their Java E-Commerce application.


If you like this article, please leave a like or a comment. Let us know if you would like to see more of similar content on our blogs.

Cover image credit: Photo by Paul Felberbauer on Unsplash

Posted on Dec 4 '19 by:

deepu105 profile

Deepu K Sasidharan

@deepu105

JHipster co-lead, Polyglot dev, Cloud Native Advocate, Developer Advocate @Adyen, Author, Speaker, Software craftsman. Loves simple & beautiful code. bit.ly/JHIPSTER-BOOKS

Adyen

Experience the all-in-one payments platform that grows your business from day one. - All popular payment methods - Payments around the world - In-depth data insights

Discussion

markdown guide
 

Hi Deepu, Thanks for the wonderful demonstration of extending Jhipster to a more real-world application. I am getting the following error in webpack. Hope you can help me out.

─$ ./gradlew

Task :webpack

store@0.0.1-SNAPSHOT webpack:build /Users/akd/jhip-ecomm
npm run cleanup && npm run webpack:build:main

store@0.0.1-SNAPSHOT cleanup /Users/akd/jhip-ecomm
rimraf build/resources/main/static/

store@0.0.1-SNAPSHOT webpack:build:main /Users/akd/jhip-ecomm
npm run webpack -- --config webpack/webpack.dev.js --env.stats=minimal

store@0.0.1-SNAPSHOT webpack /Users/akd/jhip-ecomm
node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js "--config" "webpack/webpack.dev.js" "--env.stats=minimal"

Webpack: Starting ...
Starting type checking service...
Webpack: Starting ...

βœ” Compile modules
βœ” Build modules
βœ” Optimize modules
βœ” Emit files

Webpack: Finished after 16.807 seconds.

:webpack
ERROR Failed to compile with 23 errors9:07:59 AM

error in /Users/akd/jhip-ecomm/node_modules/jest-diff/build/diffLines.d.ts

ERROR in /Users/akd/jhip-ecomm/node_modules/jest-diff/build/diffLines.d.ts(8,15):
TS2304: Cannot find name 'DiffOptions'.

error in /Users/akd/jhip-ecomm/node_modules/jest-diff/build/diffLines.d.ts

.....

ERROR in /Users/akd/jhip-ecomm/src/main/webapp/app/modules/login/login.tsx
ERROR in /Users/akd/jhip-ecomm/src/main/webapp/app/modules/login/login.tsx(26,11):
TS2339: Property 'from' does not exist on type '{} | { from: { pathname: string; search: string; }; }'.
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! store@0.0.1-SNAPSHOT webpack: node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js "--config" "webpack/webpack.dev.js" "--env.stats=minimal"
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the store@0.0.1-SNAPSHOT webpack script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /Users/anidhula/.npm/_logs/2020-06-25T03_37_59_252Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! store@0.0.1-SNAPSHOT webpack:build:main: npm run webpack -- --config webpack/webpack.dev.js --env.stats=minimal
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the store@0.0.1-SNAPSHOT webpack:build:main script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /Users/anidhula/.npm/_logs/2020-06-25T03_37_59_337Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! store@0.0.1-SNAPSHOT webpack:build: npm run cleanup && npm run webpack:build:main
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the store@0.0.1-SNAPSHOT webpack:build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /Users/anidhula/.npm/_logs/2020-06-25T03_37_59_360Z-debug.log

Task :webpack FAILED

FAILURE: Build failed with an exception.

  • What went wrong:
    Execution failed for task ':webpack'.

    Process 'command 'npm'' finished with non-zero exit value 2

  • Try:
    Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

  • Get more help at help.gradle.org

BUILD FAILED in 24s
3 actionable tasks: 1 executed, 2 up-to-date

 

It seems like a lib version Issue, and I see you got it resolved. Let me know if you find any other issue

 

I was able to resolve when we updated Node and Jhipster versions

$ jhipster --version
INFO! Using JHipster version installed locally in current project's node_modules
6.9.1

$ node -v
v12.18.1