Fast abstract ↬

Having an e-commerce retailer is essential for any retailer proprietor as an increasing number of prospects are turning to on-line buying. On this tutorial, we will undergo easy methods to create an e-commerce web site with Angular 11. The location will use the Commerce Layer as a headless e-commerce API and use Paypal to course of funds.

These days it’s important to have a web based presence when working a enterprise. Much more buying is completed on-line than in earlier years. Having an e-commerce retailer permits store house owners to open up different streams of income they couldn’t make the most of with only a brick and mortar retailer. Different store house owners nonetheless, run their companies on-line fully with out a bodily presence. This makes having a web based retailer essential.

Websites resembling Etsy, Shopify and Amazon make it simple to arrange a retailer fairly shortly with out having to fret about creating a web site. Nevertheless, there could also be cases the place store house owners could desire a customized expertise or possibly save on the price of proudly owning a retailer on a few of these platforms.

Headless e-commerce API platforms present backends that retailer websites can interface with. They handle all processes and knowledge associated to the shop like buyer, orders, shipments, funds, and so forth. All that’s wanted is a frontend to work together with this info. This provides house owners lots of flexibility on the subject of deciding how their prospects will expertise their on-line retailer and the way they select to run it.

On this article, we’ll cowl easy methods to construct an e-commerce retailer utilizing Angular 11. We will use Commerce Layer as our headless e-commerce API. Though there could also be tonnes of how to course of funds, we’ll exhibit easy methods to use only one, Paypal.

Stipulations

Earlier than constructing the app, you might want to have Angular CLI put in. We will use it to initialize and scaffold the app. When you don’t have it put in but, you will get it via npm.

npm set up -g @angular/cli

You’ll additionally want a Commerce Layer developer account. Utilizing the developer account, you have to to create a take a look at group and seed it with take a look at knowledge. Seeding makes it simpler to develop the app first with out worrying about what knowledge you’ll have to make use of. You’ll be able to create an account at this link and a corporation here.

Commerce Layer developer account organizations dashboard

Commerce Layer developer account organizations dashboard the place you add your group. (Large preview)

Commerce Layer organizations creation form

Examine the Seed with take a look at knowledge field when creating a brand new group. (Large preview)

Lastly, you have to a Paypal Sandbox account. Having any such account will enable us to check transactions between companies and customers with out risking precise cash. You’ll be able to create one here. A sandbox account has a take a look at enterprise and take a look at private account already created for it.

Extra after soar! Proceed studying beneath ↓

Commerce Layer And Paypal Config

To make Paypal Sandbox funds potential on Commerce Layer, you’ll have to arrange API keys. Head on over to the accounts overview of your Paypal developer account. Choose a enterprise account and below the API credentials tab of the account particulars, you will discover the Default Utility below REST Apps.

API Credentials tab on Paypal Sandbox business account details pop-up

The place to search out the default REST app on the Paypal enterprise account particulars pop-up. (Large preview)

Default Application overview on Paypal Sandbox business account settings

Default Utility overview on Paypal Sandbox enterprise account settings the place you will get the REST API consumer Id and secret. (Large preview)

To affiliate your Paypal enterprise account together with your Commerce Layer group, go to your group’s dashboard. Right here you’ll add a Paypal fee gateway and a Paypal fee technique to your varied markets. Below Settings > Funds, choose Cost Gateways > Paypal and add your Paypal consumer Id and secret.

New Payments Gateway dashboard on Commerce Layer

The place on Commerce Layer dashboard to create a Paypal funds gateway. (Large preview)

After creating the gateway, you have to to create a Paypal fee technique for every market you might be focusing on to make Paypal obtainable as an choice. You’ll do that below Settings > Funds > Cost Strategies > New Cost Technique.

Payments Methods dashboard on Commerce Layer

The place on Commerce Layer dashboard to create a Paypal funds technique. (Large preview)

A Be aware About Routes Used

Commerce Layer gives a route for authentication and one other completely different set of routes for his or her API. Their /oauth/token authentication route exchanges credentials for a token. This token is required to entry their API. The remainder of the API routes take the sample /api/:useful resource.

The scope of this text solely covers the frontend portion of this app. I opted to retailer the tokens server aspect, use classes to trace possession, and supply http-only cookies with a session id to the consumer. This won’t be lined right here as it’s exterior the scope of this text. Nevertheless, the routes stay the identical and precisely correspond to the Commerce Layer API. Though, there are a few customized routes not obtainable from the Commerce Layer API that we’ll use. These primarily take care of session administration. I’ll level these out as we get to them and describe how one can obtain the same consequence.

One other inconsistency it’s possible you’ll discover is that the request our bodies differ from what the Commerce Layer API requires. For the reason that requests are handed on to a different server to get populated with a token, I structured the our bodies in another way. This was to make it simpler to ship requests. At any time when there are any inconsistencies within the request our bodies, these will likely be identified within the companies.

Since that is out of scope, you’ll have to determine easy methods to retailer tokens securely. You’ll additionally have to barely modify request our bodies to match precisely what the Commerce Layer API requires. When there may be an inconsistency, I’ll hyperlink to the API reference and guides detailing easy methods to appropriately construction the physique.

App Construction

To arrange the app, we’ll break it down into 4 important components. A greater description of what every of the modules does is given below their corresponding sections:

  1. the core module,
  2. the information module,
  3. the shared module,
  4. the function modules.

The function modules will group associated pages and elements collectively. There will likely be 4 function modules:

  1. the auth module,
  2. the product module,
  3. the cart module,
  4. the checkout module.

As we get to every module, I’ll clarify what its objective is and break down its contents.

Beneath is a tree of the src/app folder and the place every module resides.

src
├── app
│   ├── core
│   ├── knowledge
│   ├── options
│   │   ├── auth
│   │   ├── cart
│   │   ├── checkout
│   │   └── merchandise
└── shared

Producing The App And Including Dependencies

We’ll start by producing the app. Our group will likely be known as The LIme Model and could have take a look at knowledge already seeded by Commerce Layer.

ng new lime-app

We’ll want a few dependencies. Primarily Angular Material and Until Destroy. Angular Materials will present elements and styling. Till Destroy routinely unsubscribes from observables when elements are destroyed. To put in them run:

npm set up @ngneat/until-destroy
ng add @angular/materials

Belongings

When including addresses to Commerce Layer, an alpha-2 nation code must be used. We’ll add a json file containing these codes to the property folder at property/json/country-codes.json. You could find this file linked here.

Kinds

The elements we’ll create share some world styling. We will place them in types.css which may be discovered at this link.

Surroundings

Our configuration will include two fields. The apiUrl which ought to level to the Commerce Layer API. apiUrl is utilized by the companies we’ll create to fetch knowledge. The clientUrl ought to be the area the app is working on. We use this when setting redirect URLs for Paypal. You could find this file at this link.

Shared Module

The shared module will comprise companies, pipes, and elements shared throughout the opposite modules.

ng g m shared

It consists of three elements, one pipe, and two companies. Right here’s what that may seem like.

src/app/shared
├── elements
│   ├── item-quantity
│   │   ├── item-quantity.part.css
│   │   ├── item-quantity.part.html
│   │   └── item-quantity.part.ts
│   ├── simple-page
│   │   ├── simple-page.part.css
│   │   ├── simple-page.part.html
│   │   └── simple-page.part.ts
│   └── title
│       ├── title.part.css
│       ├── title.part.html
│       └── title.part.ts
├── pipes
│   └── word-wrap.pipe.ts
├── companies
│   ├── http-error-handler.service.ts
│   └── local-storage.service.ts
└── shared.module.ts

We will additionally use the shared module to export some generally used Angular Materials elements. This makes it simpler to make use of them out of the field as a substitute of importing every part throughout varied modules. Right here’s what shared.module.ts will comprise.

@NgModule(
  declarations: [SimplePageComponent, TitleComponent, WordWrapPipe, ItemQuantityComponent],
  imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, RouterModule],
  exports: [
    CommonModule,
    ItemQuantityComponent,
    MatButtonModule,
    MatIconModule,
    MatSnackBarModule,
    MatTooltipModule,
    SimplePageComponent,
    TitleComponent,
    WordWrapPipe
  ]
)
export class SharedModule  

Parts

Merchandise Amount Part

This part units the amount of things when including them to the cart. It is going to be used within the cart and merchandise modules. A fabric selector would have been a simple alternative for this objective. Nevertheless, the type of the fabric choose didn’t match the fabric inputs utilized in all the opposite kinds. A fabric menu appeared similar to the fabric inputs used. So I made a decision to create a choose part with it as a substitute.

ng g c shared/elements/item-quantity

The part could have three enter properties and one output property. amount units the preliminary amount of things, maxValue signifies the utmost variety of gadgets that may be chosen in a single go, and disabled signifies whether or not the part ought to be disabled or not. The setQuantityEvent is triggered when a amount is chosen.

When the part is initialized, we’ll set the values that seem on the fabric menu. There additionally exists a way known as setQuantity that may emit setQuantityEvent occasions.

This is the part file.

@Part(
  selector: 'app-item-quantity',
  templateUrl: './item-quantity.part.html',
  styleUrls: ['./item-quantity.component.css']
)
export class ItemQuantityComponent implements OnInit 
  @Enter() amount: quantity = 0;
  @Enter() maxValue?: quantity = 0;
  @Enter() disabled?: boolean = false;
  @Output() setQuantityEvent = new EventEmitter<quantity>();

  values: quantity[] = [];

  constructor()  

  ngOnInit() 
    if (this.maxValue) 
      for (let i = 1; i <= this.maxValue; i++) 
        this.values.push(i);
      
    
  

  setQuantity(worth: quantity) 
    this.setQuantityEvent.emit(worth);
  

That is its template.

<button mat-stroked-button [matMenuTriggerFor]="menu" [disabled]="disabled">
    amount
    <mat-icon *ngIf="!disabled">expand_more</mat-icon>
</button>
<mat-menu #menu="matMenu">
    <button *ngFor="let no of values" (click on)="setQuantity(no)" mat-menu-item>no</button>
</mat-menu>

Right here is its styling.

button 
    margin: 3px;

Title Part

This part doubles as a stepper title in addition to a plain title on some less complicated pages. Though Angular Materials gives a stepper part, it wasn’t the most effective match for a quite lengthy checkout course of, wasn’t as responsive on smaller shows, and required much more time to implement. A less complicated title nonetheless may very well be repurposed as a stepper indicator and be helpful throughout a number of pages.

ng g c shared/elements/title

The part has 4 enter properties: a title, a subtitle, a quantity (no), and centerText, to point whether or not to heart the textual content of the part.

@Part(
  selector: 'app-title',
  templateUrl: './title.part.html',
  styleUrls: ['./title.component.css']
)
export class TitleComponent 
  @Enter() title: string = '';
  @Enter() subtitle: string = '';
  @Enter() no?: string;
  @Enter() centerText?: boolean = false;

Beneath is its template. You could find its styling linked here.

<div id="header">
    <h1 *ngIf="no" class="mat-display-1" id="no">no</h1>
    <div [ngClass]=" 'centered-section': centerText">
        <h1 class="mat-display-2">title</h1>
        <p id="subheading">subtitle</p>
    </div>
</div>

Easy Web page Part

There are a number of cases the place a title, an icon, and a button had been all that had been wanted for a web page. These embody a 404 web page, an empty cart web page, an error web page, a fee web page, and an order placement web page. That’s the aim the straightforward web page part will serve. When the button on the web page is clicked, it should both redirect to a route or carry out some motion in response to a buttonEvent.

To make it:

ng g c shared/elements/simple-page

This is its part file.

@Part(
  selector: 'app-simple-page',
  templateUrl: './simple-page.part.html',
  styleUrls: ['./simple-page.component.css']
)
export class SimplePageComponent  undefined;
  @Output() buttonEvent = new EventEmitter();

  constructor(non-public router: Router)  

  buttonClicked() 
    if (this.route) 
      this.router.navigateByUrl(this.route);
     else 
      this.buttonEvent.emit();
    
  

And its template incorporates:

<div id="container">
    <app-title no="quantity" title="title" subtitle="subtitle" [centerText]="centerText"></app-title>
    <div *ngIf="icon" id="icon-container">
        <mat-icon shade="main" class="icon">icon</mat-icon>
    </div>
    <button mat-flat-button shade="main" (click on)="buttonClicked()" [disabled]="buttonDisabled">
        buttonText
    </button>
</div>

It’s styling may be discovered here.

Pipes

Phrase Wrap Pipe

Some merchandise’ names and different sorts of info displayed on the positioning are actually lengthy. In some cases, getting these lengthy sentences to wrap in materials elements is difficult. So we’ll use this pipe to chop the sentences right down to a specified size and add ellipses to the tip of the consequence.

To create it run:

ng g pipe shared/pipes/word-wrap

It can comprise:

import  Pipe, PipeTransform  from '@angular/core';

@Pipe(
  title: 'wordWrap'
)
export class WordWrapPipe implements PipeTransform 
  remodel(worth: string, size: quantity): string 
    return `$worth.substring(0, size)...`;
  

Providers

HTTP Error Handler Service

There are fairly quite a lot of http companies on this challenge. Creating an error handler for every technique is repetitive. So creating one single handler that can be utilized by all strategies is smart. The error handler can be utilized to format an error and likewise cross on the errors to different exterior logging platforms.

Generate it by working:

ng g s shared/companies/http-error-handler

This service will comprise just one technique. The tactic will format the error message to be displayed relying on whether or not it’s a consumer or server error. Nevertheless, there may be room to enhance it additional.

@Injectable(
  providedIn: 'root'
)
export class HttpErrorHandler 

  constructor()  

  handleError(err: HttpErrorResponse): Observable 
    let displayMessage="";

    if (err.error instanceof ErrorEvent) 
      displayMessage = `Consumer-side error: $err.error.message`;
     else 
      displayMessage = `Server-side error: $err.message`;
    

    return throwError(displayMessage);
  

Native Storage Service

We will use native storage to maintain monitor of the variety of gadgets in a cart. It’s additionally helpful to retailer the Id of an order right here. An order corresponds to a cart on Commerce Layer.

To generate the native storage service run:

ng g s shared/companies/local-storage

The service will comprise 4 strategies so as to add, delete, and get gadgets from native storage and one other to clear it.

import  Injectable  from '@angular/core';

@Injectable(
  providedIn: 'root'
)
export class LocalStorageService 

  constructor()  

  addItem(key: string, worth: string) 
    localStorage.setItem(key, worth);
  

  deleteItem(key: string) 
    localStorage.removeItem(key);
  

  getItem(key: string): string 

Knowledge Module

This module is accountable for knowledge retrieval and administration. It’s what we’ll use to get the information our app consumes. Beneath is its construction:

src/app/knowledge
├── knowledge.module.ts
├── fashions
└── companies

To generate the module run:

ng g m knowledge

Fashions

The fashions outline how the information we devour from the API is structured. We’ll have 16 interface declarations. To create them run:

for mannequin in 
deal with cart nation customer-address 
buyer delivery-lead-time line-item order 
payment-method payment-source paypal-payment 
value cargo shipping-method sku stock-location; 
do ng g interface "knowledge/fashions/$mannequin"; executed

The next desk hyperlinks to every file and provides an outline of what every interface is.

Interface Description
Address Represents a normal deal with.
Cart Consumer aspect model of an order monitoring the variety of merchandise a buyer intends to buy.
Country Alpha-2 nation code.
Customer Address An deal with related to a buyer.
Customer A registered person.
Delivery Lead Time Represents the period of time it should take to supply a cargo.
Line Item An itemized product added to the cart.
Order A buying cart or assortment of line gadgets.
Payment Method A fee sort made obtainable to an order.
Payment Source A fee related to an order.
Paypal Payment A fee made via Paypal
Price Value related to an SKU.
Shipment Assortment of things shipped collectively.
Shipping Method Technique via which a package deal is shipped.
SKU A singular stock-keeping unit.
Stock Location Location that incorporates SKU stock.

Providers

This folder incorporates the companies that create, retrieve, and manipulate app knowledge. We’ll create 11 companies right here.

for service in 
deal with cart nation customer-address 
buyer delivery-lead-time line-item 
order paypal-payment cargo sku; 
do ng g s "knowledge/companies/$service"; executed

Tackle Service

This service creates and retrieves addresses. It’s necessary when creating and assigning transport and billing addresses to orders. It has two strategies. One to create an deal with and one other to retrieve one.

The route used right here is /api/addresses. When you’re going to make use of the Commerce Layer API straight, be sure to construction the information as demonstrated in this example.

@Injectable(
  providedIn: 'root'
)
export class AddressService 
  non-public url: string = `$setting.apiUrl/api/addresses`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  createAddress(deal with: Tackle): Observable<Tackle> 
    return this.http.submit<Tackle>(this.url, deal with)
      .pipe(catchError(this.eh.handleError));
  

  getAddress(id: string): Observable<Tackle> 
    return this.http.get<Tackle>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

Cart Service

The cart is accountable for sustaining the amount of things added and the order Id. Making API calls to get the variety of gadgets in an order everytime a brand new line merchandise is created may be costly. As a substitute, we may simply use native storage to keep up the rely on the consumer. This eliminates the necessity to make pointless order fetches each time an merchandise is added to the cart.

We additionally use this service to retailer the order Id. A cart corresponds to an order on Commerce Layer. As soon as the primary merchandise is added to the cart, an order is created. We have to protect this order Id so we will fetch it through the checkout course of.

Moreover, we’d like a option to talk to the header that an merchandise has been added to the cart. The header incorporates the cart button and shows the quantity of things in it. We’ll use an observable of a BehaviorSubject with the present worth of the cart. The header can subscribe to this and monitor adjustments within the cart worth.

Lastly, as soon as an order has been accomplished the cart worth must be cleared. This ensures that there’s no confusion when creating subsequent newer orders. The values that had been saved are cleared as soon as the present order is marked as positioned.

We’ll accomplish all this utilizing the native storage service created earlier.

@Injectable(
  providedIn: 'root'
)
export class CartService 
  non-public cart = new BehaviorSubject(
    orderId: this.orderId,
    itemCount: this.itemCount
  );

  cartValue$ = this.cart.asObservable();

  constructor(non-public storage: LocalStorageService)  

  get orderId(): string 
    const id = this.storage.getItem('order-id');
    return id ? id : '';
  

  set orderId(id: string) 
    this.storage.addItem('order-id', id);
    this.cart.subsequent( orderId: id, itemCount: this.itemCount );
  

  get itemCount(): quantity 
    const itemCount = this.storage.getItem('item-count');

    return itemCount ? parseInt(itemCount) : 0;
  

  set itemCount(quantity: quantity) 
    this.storage.addItem('item-count', quantity.toString());
    this.cart.subsequent( orderId: this.orderId, itemCount: quantity );
  

  incrementItemCount(quantity: quantity) 
    this.itemCount = this.itemCount + quantity;
  

  decrementItemCount(quantity: quantity) 
    this.itemCount = this.itemCount - quantity;
  

  clearCart() 
    this.storage.deleteItem('item-count');
    this.cart.subsequent( orderId: '', itemCount: 0 );
  

Nation Service

When including addresses on Commerce Layer, the nation code needs to be an alpha 2 code. This service reads a json file containing these codes for each nation and returns it in its getCountries technique.

@Injectable(
  providedIn: 'root'
)
export class CountryService 

  constructor(non-public http: HttpClient)  

  getCountries(): Observable 
    return this.http.get('./../../../property/json/country-codes.json');
  

Buyer Tackle Service

This service is used to affiliate addresses with prospects. It additionally fetches a selected or all addresses associated to a buyer. It’s used when the shopper provides their transport and billing addresses to their order. The createCustomer technique creates a buyer, getCustomerAddresses will get all of a buyer’s addresses, and getCustomerAddress will get a selected one.

When making a buyer deal with, make sure to construction the submit physique in line with this example.

@Injectable(
  providedIn: 'root'
)
export class CustomerAddressService 
  non-public url: string = `$setting.apiUrl/api/customer_addresses`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  createCustomerAddress(addressId: string, customerId: string): Observable<CustomerAddress> 
    return this.http.submit<CustomerAddress>(this.url, 
      addressId: addressId, customerId: customerId
    )
      .pipe(catchError(this.eh.handleError));
  

  getCustomerAddresses(): Observable<CustomerAddress[]> 
    return this.http.get<CustomerAddress[]>(`$this.url`)
      .pipe(catchError(this.eh.handleError));
  

  getCustomerAddress(id: string): Observable<CustomerAddress> 
    return this.http.get<CustomerAddress>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

Buyer Service

Clients are created and their info retrieved utilizing this service. When a person indicators up, they change into a buyer and are created utilizing the createCustomerMethod. getCustomer returns the shopper related to a selected Id. getCurrentCustomer returns the shopper at the moment logged in.

When making a buyer, construction the information like this. You’ll be able to add their first and final names to the metadata, as proven in its attributes.

The route /api/prospects/present is just not obtainable on Commerce Layer. So that you’ll want to determine easy methods to get the at the moment logged in buyer.

@Injectable(
  providedIn: 'root'
)
export class CustomerService 
  non-public url: string = `$setting.apiUrl/api/prospects`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  createCustomer(e mail: string, password: string, firstName: string, lastName: string): Observable<Buyer> 
    return this.http.submit<Buyer>(this.url, 
      e mail: e mail,
      password: password,
      firstName: firstName,
      lastName: lastName
    )
      .pipe(catchError(this.eh.handleError));
  

  getCurrentCustomer(): Observable<Buyer> 
    return this.http.get<Buyer>(`$this.url/present`)
      .pipe(catchError(this.eh.handleError));
  

  getCustomer(id: string): Observable<Buyer> 
    return this.http.get<Buyer>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

Supply Lead Time Service

This service returns details about transport timelines from varied inventory places.

@Injectable(
  providedIn: 'root'
)
export class DeliveryLeadTimeService 
  non-public url: string = `$setting.apiUrl/api/delivery_lead_times`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  getDeliveryLeadTimes(): Observable<DeliveryLeadTime[]> 
    return this.http.get<DeliveryLeadTime[]>(this.url)
      .pipe(catchError(this.eh.handleError));
  

Line Merchandise Service

Objects added to the cart are managed by this service. With it, you may create an merchandise the second it’s added to the cart. An merchandise’s info may also be fetched. The merchandise can also be up to date when its amount adjustments or deleted when faraway from the cart.

When creating gadgets or updating them, construction the request physique as proven on this example.

@Injectable(
  providedIn: 'root'
)
export class LineItemService 
  non-public url: string = `$setting.apiUrl/api/line_items`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  createLineItem(lineItem: LineItem): Observable<LineItem> 
    return this.http.submit<LineItem>(this.url, lineItem)
      .pipe(catchError(this.eh.handleError));
  

  getLineItem(id: string): Observable<LineItem> 
    return this.http.get<LineItem>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

  updateLineItem(id: string, amount: quantity): Observable<LineItem> 
    return this.http.patch<LineItem>(`$this.url/$id`,  amount: amount )
      .pipe(catchError(this.eh.handleError));
  

  deleteLineItem(id: string): Observable<LineItem> 
    return this.http.delete<LineItem>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

Order Service

Just like the road merchandise service, the order service means that you can create, replace, delete, or get an order. Moreover, it’s possible you’ll select to get the shipments related to an order individually utilizing the getOrderShipments technique. This service is used closely all through the checkout course of.

There are completely different sorts of details about an order which might be required all through checkout. Since it might be costly to fetch an entire order and its relations, we specify what we need to get from an order utilizing GetOrderParams. The equal of this on the CL API is the include query parameter the place you record the order relationships to be included. You’ll be able to verify what fields should be included for the cart abstract here and for the assorted checkout phases here.

In the identical method, when updating an order, we use UpdateOrderParams to specify replace fields. It’s because within the server that populates the token, some additional operations are carried out relying on what subject is being up to date. Nevertheless, should you’re making direct requests to the CL API, you don’t want to specify this. You are able to do away with it for the reason that CL API doesn’t require you to specify them. Though, the request physique ought to resemble this example.

@Injectable(
  providedIn: 'root'
)
export class OrderService 
  non-public url: string = `$setting.apiUrl/api/orders`;

  constructor(
    non-public http: HttpClient,
    non-public eh: HttpErrorHandler)  

  createOrder(): Observable<Order> 
    return this.http.submit<Order>(this.url, )
      .pipe(catchError(this.eh.handleError));
  

  getOrder(id: string, orderParam: GetOrderParams): Observable<Order> 
    let params = ;
    if (orderParam != GetOrderParams.none) 
      params =  [orderParam]: 'true' ;
    

    return this.http.get<Order>(`$this.url/$id`,  params: params )
      .pipe(catchError(this.eh.handleError));
  

  updateOrder(order: Order, params: UpdateOrderParams[]): Observable<Order> 
    let updateParams = [];
    for (const param of params) 
      updateParams.push(param.toString());
    

    return this.http.patch<Order>(
      `$this.url/$order.id`,
      order,
       params:  'subject': updateParams  
    )
      .pipe(catchError(this.eh.handleError));
  

  getOrderShipments(id: string): Observable<Cargo[]> 
    return this.http.get<Cargo[]>(`$this.url/$id/shipments`)
      .pipe(catchError(this.eh.handleError));
  

Paypal Cost Service

This service is accountable for creating and updating Paypal funds for orders. Moreover, we will get a Paypal fee given its id. The submit physique ought to have a construction much like this example when making a Paypal fee.

@Injectable(
  providedIn: 'root'
)
export class PaypalPaymentService 
  non-public url: string = `$setting.apiUrl/api/paypal_payments`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  createPaypalPayment(fee: PaypalPayment): Observable<PaypalPayment> 
    return this.http.submit<PaypalPayment>(this.url, fee)
      .pipe(catchError(this.eh.handleError));
  

  getPaypalPayment(id: string): Observable<PaypalPayment> 
    return this.http.get<PaypalPayment>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

  updatePaypalPayment(id: string, paypalPayerId: string): Observable<PaypalPayment> 
    return this.http.patch<PaypalPayment>(
      `$this.url/$id`,
       paypalPayerId: paypalPayerId 
    )
      .pipe(catchError(this.eh.handleError));
  

Cargo Service

This service will get a cargo or updates it given its id. The request physique of a cargo replace ought to look much like this example.

@Injectable(
  providedIn: 'root'
)
export class ShipmentService 
  non-public url: string = `$setting.apiUrl/api/shipments`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  getShipment(id: string): Observable<Cargo> 
    return this.http.get<Cargo>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

  updateShipment(id: string, shippingMethodId: string): Observable<Cargo> 
    return this.http.patch<Cargo>(
      `$this.url/$id`,
       shippingMethodId: shippingMethodId 
    )
      .pipe(catchError(this.eh.handleError));
  

SKU Service

The SKU service will get merchandise from the shop. If a number of merchandise are being retrieved, they are often paginated and have a web page dimension set. Web page dimension and web page quantity ought to be set as question params like in this example should you’re making direct requests to the API. A single product may also be retrieved given its id.

@Injectable(
  providedIn: 'root'
)
export class SkuService 
  non-public url: string = `$setting.apiUrl/api/skus`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  getSku(id: string): Observable<Sku> 
    return this.http.get<Sku>(`$this.url/$id`)
      .pipe(catchError(this.eh.handleError));
  

  getSkus(web page: quantity, pageSize: quantity): Observable<Sku[]> 
    return this.http.get<Sku[]>(
      this.url,
      
        params: 
          'web page': web page.toString(),
          'pageSize': pageSize.toString()
        
      )
      .pipe(catchError(this.eh.handleError));
  

Core Module

The core module incorporates every little thing central to and customary throughout the appliance. These embody elements just like the header and pages just like the 404 web page. Providers accountable for authentication and session administration additionally fall right here, in addition to app-wide interceptors and guards.

The core module tree will seem like this.

src/app/core
├── elements
│   ├── error
│   │   ├── error.part.css
│   │   ├── error.part.html
│   │   └── error.part.ts
│   ├── header
│   │   ├── header.part.css
│   │   ├── header.part.html
│   │   └── header.part.ts
│   └── not-found
│       ├── not-found.part.css
│       ├── not-found.part.html
│       └── not-found.part.ts
├── core.module.ts
├── guards
│   └── empty-cart.guard.ts
├── interceptors
│   └── choices.interceptor.ts
└── companies
    ├── authentication.service.ts
    ├── header.service.ts
    └── session.service.ts

To generate the module and its contents run:

ng g m core
ng g g core/guards/empty-cart
ng g s core/header/header
ng g interceptor core/interceptors/choices
for comp in header error not-found; do ng g c "core/$comp"; executed
for serv in authentication session; do ng g s "core/authentication/$serv"; executed

The core module file ought to like this. Be aware that routes have been registered for the NotFoundComponent and ErrorComponent.

@NgModule(
  declarations: [HeaderComponent, NotFoundComponent, ErrorComponent],
  imports: [
    RouterModule.forChild([
       path: '404', component: NotFoundComponent ,
       path: 'error', component: ErrorComponent ,
       path: '**', redirectTo: '/404' 
    ]),
    MatBadgeModule,
    SharedModule
  ],
  exports: [HeaderComponent]
)
export class CoreModule  

Providers

The companies folder holds the authentication, session, and header companies.

Authentication Service

The AuthenticationService means that you can purchase client and customer tokens. These tokens are used to entry the remainder of the API’s routes. Buyer tokens are returned when a person exchanges an e mail and password for it and have a wider vary of permissions. Consumer tokens are issued while not having credentials and have narrower permissions.

getClientSession will get a consumer token. login will get a buyer token. Each strategies additionally create a session. The physique of a consumer token request ought to look like this and that of a buyer token like this.

@Injectable(
  providedIn: 'root'
)
export class AuthenticationService 
  non-public url: string = `$setting.apiUrl/oauth/token`;

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  getClientSession(): Observable<object> 
    return this.http.submit<object>(
      this.url,
       grantType: 'client_credentials' )
      .pipe(catchError(this.eh.handleError));
  

  login(e mail: string, password: string): Observable<object> 
    return this.http.submit<object>(
      this.url,
       username: e mail, password: password, grantType: 'password' )
      .pipe(catchError(this.eh.handleError));
  

Session Service

The SessionService is accountable for session administration. The service will comprise an observable from a BehaviorSubject known as loggedInStatus to speak whether or not a person is logged in. setLoggedInStatus units the worth of this topic, true for logged in, and false for not logged in. isCustomerLoggedIn makes a request to the server to verify if the person has an present session. logout destroys the session on the server. The final two strategies entry routes which might be distinctive to the server that populates the request with a token. They don’t seem to be obtainable from Commerce Layer. You’ll have to determine easy methods to implement them.

@Injectable(
  providedIn: 'root'
)
export class SessionService 
  non-public url: string = `$setting.apiUrl/session`;
  non-public isLoggedIn = new BehaviorSubject(false);

  loggedInStatus = this.isLoggedIn.asObservable();

  constructor(non-public http: HttpClient, non-public eh: HttpErrorHandler)  

  setLoggedInStatus(standing: boolean) 
    this.isLoggedIn.subsequent(standing);
  

  isCustomerLoggedIn(): Observable< message: string > 
    return this.http.get< message: string >(`$this.url/buyer/standing`)
      .pipe(catchError(this.eh.handleError));
  

  logout(): Observable< message: string > 
    return this.http.get< message: string >(`$this.url/destroy`)
      .pipe(catchError(this.eh.handleError));
  

The HeaderService is used to speak whether or not the cart, login, and logout buttons ought to be proven within the header. These buttons are hidden on the login and signup pages however current on all different pages to stop confusion. We’ll use an observable from a BehaviourSubject known as showHeaderButtons that shares this. We’ll even have a setHeaderButtonsVisibility technique to set this worth.

@Injectable(
  providedIn: 'root'
)
export class HeaderService 
  non-public headerButtonsVisibility = new BehaviorSubject(true);

  showHeaderButtons = this.headerButtonsVisibility.asObservable();

  constructor()  

  setHeaderButtonsVisibility(seen: boolean) 
    this.headerButtonsVisibility.subsequent(seen);
  

Parts

Error Part

This part is used as an error web page. It’s helpful in cases when server requests fail and completely no knowledge is displayed on a web page. As a substitute of exhibiting a clean web page, we let the person know that an issue occurred. Beneath is it’s template.

<app-simple-page title="An error occurred" subtitle="There was an issue fetching your web page" buttonText="GO TO HOME" icon="report" [centerText]="true" route="https://smashingmagazine.com/">
</app-simple-page>

That is what the part will seem like.

Screenshot of error page

Screenshot of error web page. (Large preview)

Not Discovered Part

This can be a 404 web page that the person will get redirected to after they request a route not obtainable on the router. Solely its template is modified.

<app-simple-page title="404: Web page not discovered" buttonText="GO TO HOME" icon="search" subtitle="The requested web page couldn't be discovered" [centerText]="true" route="https://smashingmagazine.com/"></app-simple-page>

Screenshot of 404 page

Screenshot of 404 web page. (Large preview)

The HeaderComponent is mainly the header displayed on the prime of a web page. It can comprise the app title, the cart, login, and logout buttons.

When this part is initialized, a request is made to verify whether or not the person has a present session. This occurs when subscribing to this.session.isCustomerLoggedIn(). We subscribe to this.session.loggedInStatus to verify if the person logs out all through the lifetime of the app. The this.header.showHeaderButtons subscription decides whether or not to indicate all of the buttons on the header or disguise them. this.cart.cartValue$ will get the rely of things within the cart.

There exists a logout technique that destroys a person’s session and assigns them a consumer token. A consumer token is assigned as a result of the session sustaining their buyer token is destroyed and a token remains to be required for every API request. A fabric snackbar communicates to the person whether or not their session was efficiently destroyed or not.

We use the @UntilDestroy( checkProperties: true ) decorator to point that each one subscriptions ought to be routinely unsubscribed from when the part is destroyed.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-header',
  templateUrl: './header.part.html',
  styleUrls: ['./header.component.css']
)
export class HeaderComponent implements OnInit 
  cartAmount: quantity = 0;
  isLoggedIn: boolean = false;
  showButtons: boolean = true;

  constructor(
    non-public session: SessionService,
    non-public snackBar: MatSnackBar,
    non-public cart: CartService,
    non-public header: HeaderService,
    non-public auth: AuthenticationService
  )  

  ngOnInit() 
    this.session.isCustomerLoggedIn()
      .subscribe(
        () => 
          this.isLoggedIn = true;
          this.session.setLoggedInStatus(true);
        
      );

    this.session.loggedInStatus.subscribe(standing => this.isLoggedIn = standing);

    this.header.showHeaderButtons.subscribe(seen => this.showButtons = seen);

    this.cart.cartValue$.subscribe(cart => this.cartAmount = cart.itemCount);
  

  logout() 
    concat(
      this.session.logout(),
      this.auth.getClientSession()
    ).subscribe(
      () => 
        this.snackBar.open('You've gotten been logged out.', 'Shut',  length: 4000 );
        this.session.setLoggedInStatus(false);
      ,
      err => this.snackBar.open('There was an issue logging you out.', 'Shut',  length: 4000 )
    );
  

Beneath is the header template and linked here is its styling.

<div id="header-container">
    <div id="left-half" routerLink="https://smashingmagazine.com/">
        <h1><span id="lime-text">Lime</span><span id="store-text">Retailer</span></h1>
    </div>
    <div id="right-half">
        <div id="button-container" *ngIf="showButtons">
            <button mat-icon-button shade="main" aria-label="buying cart">
                <mat-icon [matBadge]="cartAmount" matBadgeColor="accent" aria-label="buying cart" routerLink="/cart">shopping_cart</mat-icon>
            </button>
            <button mat-icon-button shade="main" aria-label="login" *ngIf="!isLoggedIn">
                <mat-icon aria-label="login" matTooltip="login" routerLink="/login">login</mat-icon>
            </button>
            <button mat-icon-button shade="main" aria-label="logout" *ngIf="isLoggedIn" (click on)="logout()">
                <mat-icon aria-label="logout" matTooltip="logout">logout</mat-icon>
            </button>
        </div>
    </div>
</div>

Guards

Empty Cart Guard

This guard prevents customers from accessing routes regarding checkout and billing if their cart is empty. It’s because to proceed with checkout, there must be a legitimate order. An order corresponds to a cart with gadgets in it. If there are gadgets within the cart, the person can proceed to a guarded web page. Nevertheless, if the cart is empty, the person is redirected to an empty-cart web page.

@Injectable(
  providedIn: 'root'
)
export class EmptyCartGuard implements CanActivate  boolean 

Interceptors

Choices Interceptor

This interceptor intercepts all outgoing HTTP requests and provides two choices to the request. These are a Content material-Sort header and a withCredentials property. withCredentials specifies whether or not a request ought to be despatched with outgoing credentials just like the http-only cookies that we use. We use Content material-Sort to point that we’re sending json assets to the server.

@Injectable()
export class OptionsInterceptor implements HttpInterceptor 

  constructor()  

  intercept(request: HttpRequest<any>, subsequent: HttpHandler): Observable<HttpEvent<any>> 
    request = request.clone(
      headers: request.headers.set('Content material-Sort', 'utility/json'),
      withCredentials: true
    );

    return subsequent.deal with(request);
  

Function Modules

This part incorporates the primary options of the app. As talked about earlier, the options are grouped in 4 modules: auth, product, cart, and checkout modules.

Merchandise Module

The merchandise module incorporates pages that show merchandise on sale. These embody the product web page and the product record web page. It’s structured as proven beneath.

src/app/options/merchandise
├── pages
│   ├── product
│   │   ├── product.part.css
│   │   ├── product.part.html
│   │   └── product.part.ts
│   └── product-list
│       ├── product-list.part.css
│       ├── product-list.part.html
│       └── product-list.part.ts
└── merchandise.module.ts

To generate it and its elements:

ng g m options/merchandise
ng g c options/merchandise/pages/product
ng g c options/merchandise/pages/product-list

That is the module file:

@NgModule(
  declarations: [ProductListComponent, ProductComponent],
  imports: [
    RouterModule.forChild([
       path: 'product/:id', component: ProductComponent ,
       path: '', component: ProductListComponent 
    ]),
    LayoutModule,
    MatCardModule,
    MatGridListModule,
    MatPaginatorModule,
    SharedModule
  ]
)
export class ProductsModule  

Product Checklist Part

This part shows a paginated record of obtainable merchandise on the market. It’s the first web page that’s loaded when the app begins.

The merchandise are displayed in a grid. Materials grid record is the most effective part for this. To make the grid responsive, the variety of grid columns will change relying on the display dimension. The BreakpointObserver service permits us to find out the scale of the display and assign the columns throughout initialization.

To get the merchandise, we name the getProducts technique of the SkuService. It returns the merchandise if profitable and assigns them to the grid. If not, we route the person to the error web page.

For the reason that merchandise displayed are paginated, we could have a getNextPage technique to get the extra merchandise.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-product-list',
  templateUrl: './product-list.part.html',
  styleUrls: ['./product-list.component.css']
)
export class ProductListComponent implements OnInit 

The template is proven beneath and its styling may be discovered here.

<mat-grid-list cols="cols" rowHeight="400px" gutterSize="20px" class="grid-layout">
    <mat-grid-tile *ngFor="let product of merchandise; trackBy: trackSkus">
        <mat-card>
            <img id="card-image" mat-card-image src="https://smashingmagazine.com/2021/07/ecommerce-angular11-headless-paypal/product.imageUrl" alt="product picture">
            <mat-card-content>
                <mat-card-title matTooltip="product.title">product.title </mat-card-title>
                <mat-card-subtitle>product.costs[0].compareAtAmountFloat </mat-card-subtitle>
            </mat-card-content>
            <mat-card-actions>
                <button mat-flat-button shade="main" [routerLink]="['/product', product.id]">
                    View
                </button>
            </mat-card-actions>
        </mat-card>
    </mat-grid-tile>
</mat-grid-list>
<mat-paginator [length]="size" [pageIndex]="pageIndex" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" (web page)="pageEvent = getNextPage($occasion)">
</mat-paginator>

The web page will seem like this.

Screenshot of product list page

Screenshot of product listpage. (Large preview)

Product Part

As soon as a product is chosen from the product record web page, this part shows its particulars. These embody the product’s full title, value, and outline. There’s additionally a button so as to add the merchandise to the product cart.

On initialization, we get the id of the product from the route parameters. Utilizing the id, we fetch the product from the SkuService.

When the person provides an merchandise to the cart, the addItemToCart technique is named. In it, we verify if an order has already been created for the cart. If not, a brand new one is made utilizing the OrderService. Afterwhich, a line merchandise is created within the order that corresponds to the product. If an order already exists for the cart, simply the road merchandise is created. Relying on the standing of the requests, a snackbar message is exhibited to the person.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-product',
  templateUrl: './product.part.html',
  styleUrls: ['./product.component.css']
)
export class ProductComponent implements OnInit {
  id: string = '';
  product!: Sku;
  amount: quantity = 0;

  constructor(
    non-public route: ActivatedRoute,
    non-public skus: SkuService,
    non-public location: Location,
    non-public router: Router,
    non-public header: HeaderService,
    non-public orders: OrderService,
    non-public lineItems: LineItemService,
    non-public cart: CartService,
    non-public snackBar: MatSnackBar
  )  

  ngOnInit() 
    this.route.paramMap
      .pipe(
        mergeMap(params => 
          const id = params.get('id')
          this.id = id ? id : '';

          return this.skus.getSku(this.id);
        ),
        faucet((sku) => 
          this.product = sku;
        )
      ).subscribe(
        error: (err) => this.router.navigateByUrl('/error')
      );

    this.header.setHeaderButtonsVisibility(true);
  

  addItemToCart() 
    if (this.amount > 0) 
      if (this.cart.orderId == '') 
        this.orders.createOrder()
          .pipe(
            mergeMap((order: Order) =>  '';

              return this.lineItems.createLineItem(
                orderId: order.id,
                title: this.product.title,
                imageUrl: this.product.imageUrl,
                amount: this.amount,
                skuCode: this.product.code
              );
            )
          )
          .subscribe(
            () => 
              this.cart.incrementItemCount(this.amount);
              this.showSuccessSnackBar();
            ,
            err => this.showErrorSnackBar()
          );
       else 
        this.lineItems.createLineItem(
          orderId: this.cart.orderId,
          title: this.product.title,
          imageUrl: this.product.imageUrl,
          amount: this.amount,
          skuCode: this.product.code
        ).subscribe(
          () => 
            this.cart.incrementItemCount(this.amount);
            this.showSuccessSnackBar();
          ,
          err => this.showErrorSnackBar()
        );
      
     else 
      this.snackBar.open('Choose a amount higher than 0.', 'Shut',  length: 8000 );
    
  

  setQuantity(no: quantity) 
    this.amount = no;
  

  goBack() 
    this.location.again();
  

  non-public showSuccessSnackBar() 
    this.snackBar.open('Merchandise efficiently added to cart.', 'Shut',  length: 8000 );
  

  non-public showErrorSnackBar() 
    this.snackBar.open('Failed so as to add your merchandise to the cart.', 'Shut',  length: 8000 );
  
}

The ProductComponent template is as follows and its styling is linked here.

<div id="container">
    <mat-card *ngIf="product" class="product-card">
        <img mat-card-image src="https://smashingmagazine.com/2021/07/ecommerce-angular11-headless-paypal/product.imageUrl" alt="Photograph of a product">
        <mat-card-content>
            <mat-card-title>product.title</mat-card-title>
            <mat-card-subtitle>product.costs[0].compareAtAmountFloat </mat-card-subtitle>
            <p>
                product.description
            </p>
        </mat-card-content>
        <mat-card-actions>
            <app-item-quantity [quantity]="amount" [maxValue]="10" (setQuantityEvent)="setQuantity($occasion)"></app-item-quantity>
            <button mat-raised-button shade="accent" (click on)="addItemToCart()">
                <mat-icon>add_shopping_cart</mat-icon>
                Add to cart
            </button>
            <button mat-raised-button shade="main" (click on)="goBack()">
                <mat-icon>storefront</mat-icon>
                Proceed buying
            </button>
        </mat-card-actions>
    </mat-card>
</div>

The web page will seem like this.

Screenshot of product page

Screenshot of product web page. (Large preview)

Auth Module

The Auth module incorporates pages accountable for authentication. These embody the login and signup pages. It‘s structured as follows.

src/app/options/auth/
├── auth.module.ts
└── pages
    ├── login
    │   ├── login.part.css
    │   ├── login.part.html
    │   └── login.part.ts
    └── signup
        ├── signup.part.css
        ├── signup.part.html
        └── signup.part.ts

To generate it and its elements:

ng g m options/auth
ng g c options/auth/pages/signup
ng g c options/auth/pages/login

That is its module file.

@NgModule(
  declarations: [LoginComponent, SignupComponent],
  imports: [
    RouterModule.forChild([
       path: 'login', component: LoginComponent ,
       path: 'signup', component: SignupComponent 
    ]),
    MatFormFieldModule,
    MatInputModule,
    ReactiveFormsModule,
    SharedModule
  ]
)
export class AuthModule  

Signup Part

A person indicators up for an account utilizing this part. A primary title, final title, e mail, and password are required for the method. The person additionally wants to verify their password. The enter fields will likely be created with the FormBuilder service. Validation is added to require that each one the inputs have values. Extra validation is added to the password subject to make sure a minimal size of eight characters. A customized matchPasswords validator ensures that the confirmed password matches the preliminary password.

When the part is initialized, the cart, login, and logout buttons within the header are hidden.That is communicated to the header utilizing the HeaderService.

After all of the fields are marked as legitimate, the person can then join. Within the signup technique, the createCustomer technique of the CustomerService receives this enter. If the signup is profitable, the person is knowledgeable that their account was efficiently created utilizing a snackbar. They’re then rerouted to the house web page.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-signup',
  templateUrl: './signup.part.html',
  styleUrls: ['./signup.component.css']
)
export class SignupComponent implements OnInit 
  signupForm = this.fb.group(
    firstName: ['', Validators.required],
    lastName: ['', Validators.required],
    e mail: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirmedPassword: ['', [Validators.required]]
  ,  validators: this.matchPasswords );

  @ViewChild(FormGroupDirective) sufDirective: FormGroupDirective 

Beneath is the template for the SignupComponent.

<kind id="container" [formGroup]="signupForm" (ngSubmit)="signup()">
    <h1 class="mat-display-3">Create Account</h1>
    <mat-form-field look="define">
        <mat-label>First Title</mat-label>
        <enter matInput formControlName="firstName">
        <mat-icon matPrefix>portrait</mat-icon>
    </mat-form-field>
    <mat-form-field look="define">
        <mat-label>Final Title</mat-label>
        <enter matInput formControlName="lastName">
        <mat-icon matPrefix>portrait</mat-icon>
    </mat-form-field>
    <mat-form-field look="define">
        <mat-label>E mail</mat-label>
        <enter matInput formControlName="e mail" sort="e mail">
        <mat-icon matPrefix>alternate_email</mat-icon>
    </mat-form-field>
    <mat-form-field look="define">
        <mat-label>Password</mat-label>
        <enter matInput formControlName="password" sort="password">
        <mat-icon matPrefix>vpn_key</mat-icon>
    </mat-form-field>
    <mat-form-field look="define">
        <mat-label>Affirm Password</mat-label>
        <enter matInput formControlName="confirmedPassword" sort="password">
        <mat-icon matPrefix>vpn_key</mat-icon>
    </mat-form-field>
    <div *ngIf="confirmedPassword?.invalid && (confirmedPassword?.soiled || confirmedPassword?.touched)">
        <mat-error *ngIf="signupForm.hasError('differentPasswords')">
            Your passwords don't match.
        </mat-error>
    </div>
    <div *ngIf="password?.invalid && (password?.soiled || password?.touched)">
        <mat-error *ngIf="password?.hasError('minlength')">
            Your password ought to be at the very least eight characters.
        </mat-error>
    </div>
    <button mat-flat-button shade="main" [disabled]="!signupForm.legitimate">Signal Up</button>
</kind>

The part will end up as follows.

Screenshot of signup page

Screenshot of signup web page. (Large preview)

Login Part

A registered person logs into their account with this part. An e mail and password should be entered. Their corresponding enter fields would have validation that makes them required.

Just like the SignupComponent, the cart, login, and logout buttons within the header are hidden. Their visibility is ready utilizing the HeaderService throughout part initialization.

To login, the credentials are handed to the AuthenticationService. If profitable, the login standing of the person is ready utilizing the SessionService. The person is then routed again to the web page they had been on. If unsuccessful, a snackbar is displayed with an error and the password subject is reset.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-login',
  templateUrl: './login.part.html',
  styleUrls: ['./login.component.css']
)
export class LoginComponent implements OnInit 
  loginForm = this.fb.group(
    e mail: ['', Validators.required],
    password: ['', Validators.required]
  );

  constructor(
    non-public authService: AuthenticationService,
    non-public session: SessionService,
    non-public snackBar: MatSnackBar,
    non-public fb: FormBuilder,
    non-public header: HeaderService,
    non-public location: Location
  )  

  ngOnInit() 
    this.header.setHeaderButtonsVisibility(false);
  

  login() 
    const credentials = this.loginForm.worth;

    this.authService.login(
      credentials.e mail,
      credentials.password
    ).subscribe(
      () => 
        this.session.setLoggedInStatus(true);
        this.location.again();
      ,
      err => 
        this.snackBar.open(
          'Login failed. Examine your login credentials.',
          'Shut',
           length: 6000 );

        this.loginForm.patchValue( password: '' );
      
    );
  

Beneath is the LoginComponent template.

<kind id="container" [formGroup]="loginForm" (ngSubmit)="login()">
    <h1 class="mat-display-3">Login</h1>
    <mat-form-field look="define">
        <mat-label>E mail</mat-label>
        <enter matInput sort="e mail" formControlName="e mail" required>
        <mat-icon matPrefix>alternate_email</mat-icon>
    </mat-form-field>
    <mat-form-field look="define">
        <mat-label>Password</mat-label>
        <enter matInput sort="password" formControlName="password" required>
        <mat-icon matPrefix>vpn_key</mat-icon>
    </mat-form-field>
    <button mat-flat-button shade="main" [disabled]="!loginForm.legitimate">Login</button>
    <p id="newAccount" class="mat-h3">Not registered but? <a id="newAccountLink" routerLink="/signup">Create an account.</a></p>
</kind>

Here’s a screenshot of the web page.

Screenshot of login page

Screenshot of login web page. (Large preview)

Cart Module

The cart module incorporates all pages associated to the cart. These embody the order abstract web page, a coupon and reward card code web page, and an empty cart web page. It’s structured as follows.

src/app/options/cart/
├── cart.module.ts
└── pages
    ├── codes
    │   ├── codes.part.css
    │   ├── codes.part.html
    │   └── codes.part.ts
    ├── empty
    │   ├── empty.part.css
    │   ├── empty.part.html
    │   └── empty.part.ts
    └── abstract
        ├── abstract.part.css
        ├── abstract.part.html
        └── abstract.part.ts

To generate it, run:

ng g m options/cart
ng g c options/cart/codes
ng g c options/cart/empty
ng g c options/cart/abstract

That is the module file.

@NgModule(
  declarations: [SummaryComponent, CodesComponent, EmptyComponent],
  imports: [
    RouterModule.forChild([
      
        path: '', canActivate: [EmptyCartGuard], youngsters: [
           path: 'cart', component: SummaryComponent ,
           path: 'codes', component: CodesComponent 
        ]
      ,
       path: 'empty', part: EmptyComponent 
    ]),
    MatDividerModule,
    MatFormFieldModule,
    MatInputModule,
    MatMenuModule,
    ReactiveFormsModule,
    SharedModule
  ]
)
export class CartModule  

Codes Part

As talked about earlier, this part is used so as to add any coupon or reward card codes to an order. This enables the person to use reductions to the whole of their order earlier than continuing to checkout.

There will likely be two enter fields. One for coupons and one other for reward card codes.

The codes are added by updating the order. The updateOrder technique of the OrderService updates the order with the codes. Afterwhich, each fields are reset and the person is knowledgeable of the success of the operation with a snackbar. A snackbar can also be proven when an error happens. Each the addCoupon and addGiftCard strategies name the updateOrder technique.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-codes',
  templateUrl: './codes.part.html',
  styleUrls: ['./codes.component.css']
)
export class CodesComponent  undefined;

  constructor(
    non-public cart: CartService,
    non-public order: OrderService,
    non-public snackBar: MatSnackBar
  )  

  non-public updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) 
    this.order.updateOrder(order, params)
      .subscribe(
        () => 
          this.snackBar.open(`Efficiently added $codeType code.`, 'Shut',  length: 8000 );
          this.couponCode.reset();
          this.giftCardCode.reset();
          this.codesDirective?.reset();
        ,
        err => this.snackBar.open(`There was an issue including your $codeType code.`, 'Shut',  length: 8000 )
      );
  

  addCoupon() 
    this.updateOrder( id: this.cart.orderId, couponCode: this.couponCode.worth , [UpdateOrderParams.couponCode], 'coupon');
  

  addGiftCard() 
    this.updateOrder( id: this.cart.orderId, giftCardCode: this.giftCardCode.worth , [UpdateOrderParams.giftCardCode], 'reward card');
  

The template is proven beneath and its styling may be discovered at this link.

<div id="container">
    <app-title title="Redeem a code" subtitle="Enter a coupon code or reward card" [centerText]="true"></app-title>
    <div class="input-row">
        <mat-form-field look="define">
            <mat-label>Coupon Code</mat-label>
            <enter matInput [formControl]="couponCode" required>
            <mat-icon matPrefix>card_giftcard</mat-icon>
        </mat-form-field>
        <button class="redeem" mat-flat-button shade="accent" [disabled]="couponCode.invalid" (click on)="addCoupon()">Redeem</button>
    </div>
    <div class="input-row">
        <mat-form-field look="define">
            <mat-label>Reward Card Code</mat-label>
            <enter matInput [formControl]="giftCardCode" required>
            <mat-icon matPrefix>redeem</mat-icon>
        </mat-form-field>
        <button class="redeem" mat-flat-button shade="accent" [disabled]="giftCardCode.invalid" (click on)="addGiftCard()">Redeem</button>
    </div>
    <button shade="main" mat-flat-button routerLink="/cart">
        <mat-icon>shopping_cart</mat-icon>
        CONTINUE TO CART
    </button>
</div>

Here’s a screenshot of the web page.

Screenshot of codes page

Screenshot of codes web page. (Large preview)

Empty Part

It shouldn’t be potential to take a look at with an empty cart. There must be a guard that stops customers from accessing checkout module pages with empty carts. This has already been lined as a part of the CoreModule. The guard redirects requests to checkout pages with an empty cart to the EmptyCartComponent.

It’s a quite simple part that has some textual content indicating to the person that their cart is empty. It additionally has a button that the person can click on to go to the homepage so as to add issues to their cart. So we’ll use the SimplePageComponent to show it. Right here is the template.

<app-simple-page title="Your cart is empty" subtitle="There may be at the moment nothing in your cart. Head to the house web page so as to add gadgets." buttonText="GO TO HOME PAGE" icon="shopping_basket" [centerText]="true" route="https://smashingmagazine.com/">
</app-simple-page>

Here’s a screenshot of the web page.

Screenshot of empty cart page

Screenshot of empty cart web page. (Large preview)

Abstract Part

This part summarizes the cart/order. It lists all of the gadgets within the cart, their names, portions, and photos. It moreover breaks down the price of the order together with taxes, transport, and reductions. The person ought to have the ability to view this and determine whether or not they’re happy with the gadgets and price earlier than continuing to checkout.

On initialization, the order and its line gadgets are fetched utilizing the OrderService. A person ought to have the ability to modify the road gadgets and even take away them from the order. Objects are eliminated when the deleteLineItem technique is named. In it the deleteLineItem technique of the LineItemService receives the id of the road merchandise to be deleted. If a deletion is profitable, we replace the merchandise rely within the cart utilizing the CartService.

The person is then routed to the shopper web page the place they start the method of trying out. The checkout technique does the routing.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-summary',
  templateUrl: './abstract.part.html',
  styleUrls: ['./summary.component.css']
)
export class SummaryComponent implements OnInit 
  order: Order = ;

  abstract:  undefined, id: string [] = [];

  constructor(
    non-public orders: OrderService,
    non-public lineItems: LineItemService,
    non-public cart: CartService,
    non-public snackBar: MatSnackBar,
    non-public router: Router
  )  

  ngOnInit() 
    this.orders.getOrder(this.cart.orderId, GetOrderParams.cart)
      .subscribe(
        order => this.processOrder(order),
        err => this.showOrderError('retrieving your cart')
      );
  

  non-public processOrder(order: Order) 
    this.order = order;

    this.abstract = [
       name: 'Subtotal', amount: order.formattedSubtotalAmount, id: 'subtotal' ,
       name: 'Discount', amount: order.formattedDiscountAmount, id: 'discount' ,
       name: 'Taxes (included)', amount: order.formattedTotalTaxAmount, id: 'taxes' ,
       name: 'Shipping', amount: order.formattedShippingAmount, id: 'shipping' ,
       name: 'Gift Card', amount: order.formattedGiftCardAmount, id: 'gift-card' 
    ];
  

  non-public showOrderError(msg: string) 
    this.snackBar.open(`There was an issue $msg.`, 'Shut',  length: 8000 );
  

  checkout() 
    this.router.navigateByUrl('/buyer');
  

  deleteLineItem(id: string) 
    this.lineItems.deleteLineItem(id)
      .pipe(
        mergeMap(() => this.orders.getOrder(this.cart.orderId, GetOrderParams.cart))
      ).subscribe(
        order => ,
        err => this.showOrderError('deleting your order')
      );
  

Beneath is the template and its styling is linked here.

<div class="container" *ngIf="order">
    <h3 id="order-id">Order #order.quantity (order.skusCount gadgets)</h3>
    <div class="line-item" *ngFor="let merchandise of order.lineItems">
        <div id="product-details">
            <img *ngIf="merchandise.imageUrl" class="image-xs" src="https://smashingmagazine.com/2021/07/ecommerce-angular11-headless-paypal/merchandise.imageUrl" alt="product picture">
            <div *ngIf="!merchandise.imageUrl" class="image-xs no-image"></div>
            <div id="line-details">
                <div>merchandise.title</div>
                <div> merchandise.formattedUnitAmount  </div>
            </div>
        </div>
        <div id="product-config">
            <app-item-quantity [quantity]="merchandise.amount || 0" [disabled]="true"></app-item-quantity>
            <div class="itemTotal"> merchandise.formattedTotalAmount  </div>
            <button mat-icon-button shade="warn" (click on)="deleteLineItem(merchandise.id || '')">
                <mat-icon>clear</mat-icon>
        </button>
        </div>
    </div>
    <mat-divider></mat-divider>
    <div class="costSummary">
        <div class="costItem" *ngFor="let merchandise of abstract" [id]="merchandise.id">
            <h3 class="costLabel">merchandise.title</h3>
            <p> merchandise.quantity  </p>
        </div>
    </div>
    <mat-divider></mat-divider>
    <div class="costSummary">
        <div class="costItem" id="whole">
            <h2 id="totalLabel">Complete</h2>
            <h2> order.formattedTotalAmountWithTaxes </h2>
        </div>
    </div>
    <div id="checkout-button">
        <button shade="accent" mat-flat-button routerLink="/codes">
        <mat-icon>redeem</mat-icon>
        ADD GIFT CARD/COUPON
    </button>
        <button shade="main" mat-flat-button (click on)="checkout()">
        <mat-icon>point_of_sale</mat-icon>
        CHECKOUT
    </button>
    </div>
</div>

Here’s a screenshot of the web page.

Screenshot of summary page

Screenshot of abstract web page. (Large preview)

Checkout Module

This module is accountable for the checkout course of. Checkout entails offering a billing and transport deal with, a buyer e mail, and deciding on a transport and fee technique. The final step of this course of is placement and affirmation of the order. The construction of the module is as follows.

src/app/options/checkout/
├── elements
│   ├── deal with
│   ├── address-list
│   └── country-select
└── pages
    ├── billing-address
    ├── cancel-payment
    ├── buyer
    ├── fee
    ├── place-order
    ├── shipping-address
    └── shipping-methods

This module is the largest by far and incorporates Three elements and seven pages. To generate it and its elements run:

ng g m options/checkout
for comp in 
deal with address-list country-select; do 
ng g c "options/checkout/elements/$comp" 
; executed
for web page in 
billing-address cancel-payment buyer 
fee place-order shipping-address 
shipping-methods; do 
ng g c "options/checkout/pages/$web page"; executed

That is the module file.

@NgModule(
  declarations: [
    CustomerComponent,
    AddressComponent,
    BillingAddressComponent,
    ShippingAddressComponent,
    ShippingMethodsComponent,
    PaymentComponent,
    PlaceOrderComponent,
    AddressListComponent,
    CountrySelectComponent,
    CancelPaymentComponent
  ],
  imports: [
    RouterModule.forChild([
      
        path: '', canActivate: [EmptyCartGuard], youngsters: [
           path: 'billing-address', component: BillingAddressComponent ,
           path: 'cancel-payment', component: CancelPaymentComponent ,
           path: 'customer', component: CustomerComponent ,
           path: 'payment', component: PaymentComponent ,
           path: 'place-order', component: PlaceOrderComponent ,
           path: 'shipping-address', component: ShippingAddressComponent ,
           path: 'shipping-methods', component: ShippingMethodsComponent 
        ]
      
    ]),
    MatCardModule,
    MatCheckboxModule,
    MatDividerModule,
    MatInputModule,
    MatMenuModule,
    MatRadioModule,
    ReactiveFormsModule,
    SharedModule
  ]
)
export class CheckoutModule  

Parts

Nation Choose Part

This part lets a person choose a rustic as a part of an deal with. The fabric choose part has a fairly completely different look when in comparison with the enter fields within the deal with kind. So for the sake of uniformity, a cloth menu part is used as a substitute.

When the part is initialized, the nation code knowledge is fetched utilizing the CountryService. The international locations property holds the values returned by the service. These values will likely be added to the menu within the template.

The part has one output property, setCountryEvent. When a rustic is chosen, this occasion emits the alpha-2 code of the nation.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-country-select',
  templateUrl: './country-select.part.html',
  styleUrls: ['./country-select.component.css']
)
export class CountrySelectComponent implements OnInit 
  nation: string = 'Nation';
  international locations: Nation[] = [];
  @Output() setCountryEvent = new EventEmitter<string>();

  constructor(non-public international locations: CountryService)  

  ngOnInit() 
    this.international locations.getCountries()
      .subscribe(
        international locations => 
          this.international locations = international locations;
        
      );
  

  setCountry(worth: Nation) 
    this.nation = worth.title 

Beneath is its template and linked here is its styling.

<button id="country-select" mat-stroked-button [matMenuTriggerFor]="countryMenu">
    nation
    <mat-icon>expand_more</mat-icon>
</button>
<mat-menu #countryMenu="matMenu">
    <button *ngFor="let cnt of nations" (click on)="setCountry(cnt)" mat-menu-item>cnt.title</button>
</mat-menu>

Tackle Part

This can be a kind for capturing addresses. It’s utilized by each the transport and billing deal with pages. A legitimate Commerce Layer deal with ought to comprise a primary and final title, an deal with line, a metropolis, zip code, state code, nation code, and cellphone quantity.

The FormBuilder service will create the shape group. Since this part is utilized by a number of pages, it has quite a lot of enter and output properties. The enter properties embody the button textual content, title displayed, and textual content for a checkbox. The output properties will likely be occasion emitters for when the button is clicked to create the deal with and one other for when the checkbox worth adjustments.

When the button is clicked, the addAddress technique is named and the createAddress occasion emits the entire deal with. Equally, when the checkbox is checked, the isCheckboxChecked occasion emits the checkbox worth.

@Part(
  selector: 'app-address',
  templateUrl: './deal with.part.html',
  styleUrls: ['./address.component.css']
)
export class AddressComponent  undefined;

  constructor(non-public fb: FormBuilder)  

  setCountryCode(code: string) 
    this.countryCode = code;
  

  addAddress() 
    this.createAddress.emit( 'N/A',
      countryCode: this.countryCode,
      cellphone: this.addressForm.get('cellphone')?.worth
    );
  

  setCheckboxValue(change: MatCheckboxChange) 
    if (this.isCheckboxChecked) 
      this.isCheckboxChecked.emit(change.checked);
    
  

That is its template and its styling is linked here.

<kind id="container" [formGroup]="addressForm">
    <p class="mat-headline" *ngIf="showTitle">Or add a brand new deal with</p>
    <div class="row">
        <mat-form-field look="define">
            <mat-label>First Title</mat-label>
            <enter matInput formControlName="firstName">
        </mat-form-field>
        <mat-form-field look="define">
            <mat-label>Final Title</mat-label>
            <enter matInput formControlName="lastName">
        </mat-form-field>
    </div>
    <div class="row">
        <mat-form-field look="define">
            <mat-label>Tackle</mat-label>
            <enter matInput formControlName="line1">
        </mat-form-field>
        <mat-form-field look="define">
            <mat-label>Metropolis</mat-label>
            <enter matInput formControlName="metropolis">
        </mat-form-field>
    </div>
    <div class="row">
        <mat-form-field look="define">
            <mat-label>State Code</mat-label>
            <enter matInput formControlName="stateCode">
        </mat-form-field>
        <mat-form-field look="define">
            <mat-label>Zip Code</mat-label>
            <enter matInput formControlName="zipCode">
        </mat-form-field>
    </div>
    <div class="row">
        <mat-form-field look="define">
            <mat-label>Cellphone</mat-label>
            <enter matInput formControlName="cellphone">
        </mat-form-field>
        <app-country-select (setCountryEvent)="setCountryCode($occasion)"></app-country-select>
    </div>
    <mat-checkbox shade="accent" (change)="setCheckboxValue($occasion)">
        checkboxText
    </mat-checkbox>
    <button id="submit-button" mat-flat-button shade="main" (click on)="addAddress()">
        buttonText
    </button>
</kind>

Tackle Checklist Part

When a buyer logs in, they’ll entry their present addresses. As a substitute of getting them re-enter an deal with, they’ll choose from an deal with record. That is the aim of this part. On initialization, all the shopper’s addresses are fetched utilizing the CustomerAddressService if they’re logged in. We are going to verify their login standing utilizing the SessionService.

This part has a setAddressEvent output property. When an deal with is chosen, setAddressEvent emits its id to the father or mother part.

@Part(
  selector: 'app-address-list',
  templateUrl: './address-list.part.html',
  styleUrls: ['./address-list.component.css']
)
export class AddressListComponent implements OnInit 
  addresses: CustomerAddress[] = [];

  @Output() setAddressEvent = new EventEmitter<string>();

  constructor(
    non-public session: SessionService,
    non-public customerAddresses: CustomerAddressService,
    non-public snackBar: MatSnackBar
  )  

  ngOnInit() 
    this.session.loggedInStatus
      .pipe(
        mergeMap(
          standing => iif(() => standing, this.customerAddresses.getCustomerAddresses())
        ))
      .subscribe(
        addresses => 
          if (addresses.size) 
            this.addresses = addresses
          
        ,
        err => this.snackBar.open('There was an issue getting your present addresses.', 'Shut',  length: 8000 )
      );
  

  setAddress(change: MatRadioChange) 
    this.setAddressEvent.emit(change.worth);
  

Right here is its template. You could find its styling here.

<div id="container">
    <p class="mat-headline">Choose an present deal with</p>
    <mat-error *ngIf="!addresses.size">You haven't any present addresses</mat-error>
    <mat-radio-group *ngIf="addresses.size" class="addresses" (change)="setAddress($occasion)">
        <mat-card class="deal with" *ngFor="let deal with of addresses">
            <mat-radio-button [value]="deal with.deal with?.id" shade="main">
                <p>deal with.deal with?.firstName deal with.deal with?.lastName,</p>
                <p>deal with.deal with?.line1,</p>
                <p>deal with.deal with?.metropolis,</p>
                <p>deal with.deal with?.zipCode,</p>
                <p>deal with.deal with?.stateCode, deal with.deal with?.countryCode</p>
                <p>deal with.deal with?.cellphone</p>
            </mat-radio-button>
        </mat-card>
    </mat-radio-group>
</div>

Pages

Buyer Part

An order must be related to an e mail deal with. This part is a kind that captures the shopper e mail deal with. When the part is initialized, the present buyer’s e mail deal with is fetched if they’re logged in. We get the shopper from the CustomerService. If they don’t want to change their e mail deal with, this e mail would be the default worth.

If the e-mail is modified or a buyer is just not logged in, the order is up to date with the inputted e mail. We use the OrderService to replace the order with the brand new e mail deal with. If profitable, we route the shopper to the billing deal with web page.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-customer',
  templateUrl: './buyer.part.html',
  styleUrls: ['./customer.component.css']
)
export class CustomerComponent implements OnInit 
  e mail = new FormControl('', [Validators.required, Validators.email]);

  constructor(
    non-public orders: OrderService,
    non-public prospects: CustomerService,
    non-public cart: CartService,
    non-public router: Router,
    non-public snackBar: MatSnackBar
  )  

  ngOnInit() 
    this.prospects.getCurrentCustomer()
      .subscribe(
        buyer => this.e mail.setValue(buyer.e mail)
      );
  

  addCustomerEmail() 
    this.orders.updateOrder(
       id: this.cart.orderId, customerEmail: this.e mail.worth ,
      [UpdateOrderParams.customerEmail])
      .subscribe(
        () => this.router.navigateByUrl('/billing-address'),
        err => this.snackBar.open('There was an issue including your e mail to the order.', 'Shut',  length: 8000 )
      );
  

Right here is the part template and linked here is its styling.

<div id="container">
    <app-title no="1" title="Buyer" subtitle="Billing info and transport deal with"></app-title>
    <mat-form-field look="define">
        <mat-label>E mail</mat-label>
        <enter matInput [formControl]="e mail" required>
        <mat-icon matPrefix>alternate_email</mat-icon>
    </mat-form-field>
    <button mat-flat-button shade="main" [disabled]="e mail.invalid" (click on)="addCustomerEmail()">
        PROCEED TO BILLING ADDRESS
    </button>
</div>

Here’s a screenshot of the shopper web page.

Screenshot of customer page

Screenshot of buyer web page. (Large preview)

Billing Tackle Part

The billing deal with part lets a buyer both add a brand new billing deal with or choose from their present addresses. Customers who aren’t logged in must enter a brand new deal with. Those that have logged in get an choice to choose between new or present addresses.

The showAddress property signifies whether or not present addresses ought to be proven on the part. sameShippingAddressAsBilling signifies whether or not the transport deal with ought to be the identical as what the billing deal with is ready. When a buyer selects an present deal with, then its id is assigned to selectedCustomerAddressId.

When the part is initialized, we use the SessionService to verify if the present person is logged in. If they’re logged in, we’ll show their present addresses if they’ve any.

As talked about earlier, if a person is logged in, they’ll choose an present deal with as their billing deal with. Within the updateBillingAddress technique, if they’re logged in, the deal with they choose is cloned and set because the order’s billing deal with. We do that by updating the order utilizing the updateOrder technique of the OrderService and supplying the deal with Id.

If they don’t seem to be logged in, the person has to supply an deal with. As soon as offered, the deal with is created utilizing the createAddress technique. In it, the AddressService takes the enter and makes the brand new deal with. After which, the order is up to date utilizing the id of the newly created deal with. If there may be an error or both operation is profitable, we present a snackbar.

If the identical deal with is chosen as a transport deal with, the person is routed to the transport strategies web page. In the event that they’d like to supply an alternate transport deal with, they’re directed to the transport deal with web page.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-billing-address',
  templateUrl: './billing-address.part.html',
  styleUrls: ['./billing-address.component.css']
)
export class BillingAddressComponent implements OnInit 
  showAddresses: boolean = false;
  sameShippingAddressAsBilling: boolean = false;
  selectedCustomerAddressId: string = '';

  constructor(
    non-public addresses: AddressService,
    non-public snackBar: MatSnackBar,
    non-public session: SessionService,
    non-public orders: OrderService,
    non-public cart: CartService,
    non-public router: Router,
    non-public customerAddresses: CustomerAddressService)  

  ngOnInit() 
    this.session.loggedInStatus
      .subscribe(
        standing => this.showAddresses = standing
      );
  

  updateBillingAddress(deal with: Tackle) 
    if (this.showAddresses && this.selectedCustomerAddressId) 
      this.cloneAddress();
     else if (deal with.firstName && deal with.lastName && deal with.line1 && deal with.metropolis && deal with.zipCode && deal with.stateCode && deal with.countryCode && deal with.cellphone) 
      this.createAddress(deal with);
    
    else 
      this.snackBar.open('Examine your deal with. Some fields are lacking.', 'Shut');
    
  

  setCustomerAddress(customerAddressId: string) 
    this.selectedCustomerAddressId = customerAddressId;
  

  setSameShippingAddressAsBilling(change: boolean) 
    this.sameShippingAddressAsBilling = change;
  

  non-public createAddress(deal with: Tackle) 
    this.addresses.createAddress(deal with)
      .pipe(
        concatMap(
          deal with => 
            const replace = this.updateOrderObservable(
              id: this.cart.orderId,
              billingAddressId: deal with.id
            , [UpdateOrderParams.billingAddress]);

            if (this.showAddresses) 
              return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id  else 
              return update;
            
          ))
      .subscribe(
        () => this.showSuccessSnackBar(),
        err => this.showErrorSnackBar()
      );
  

  private cloneAddress() 
    this.updateOrderObservable(
      id: this.cart.orderId,
      billingAddressCloneId: this.selectedCustomerAddressId
    , [UpdateOrderParams.billingAddressClone])
      .subscribe(
        () => this.showSuccessSnackBar(),
        err => this.showErrorSnackBar()
      );
  

  non-public updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> 
    return iif(() => this.sameShippingAddressAsBilling,
      concat([
        this.orders.updateOrder(order, updateParams),
        this.orders.updateOrder(order, [UpdateOrderParams.shippingAddressSameAsBilling])
      ]),
      this.orders.updateOrder(order, updateParams)
    );
  

  non-public showErrorSnackBar() 
    this.snackBar.open('There was an issue creating your deal with.', 'Shut',  length: 8000 );
  

  non-public navigateTo(path: string) 
    setTimeout(() => this.router.navigateByUrl(path), 4000);
  

  non-public showSuccessSnackBar() 
    this.snackBar.open('Billing deal with efficiently added. Redirecting...', 'Shut',  length: 3000 );
    if (this.sameShippingAddressAsBilling) 
      this.navigateTo('/shipping-methods');
     else 
      this.navigateTo('/shipping-address');
    
  

Right here is the template. This link factors to its styling.

<app-title no="2" title="Billing Tackle" subtitle="Tackle to invoice fees to"></app-title>
<app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($occasion)"></app-address-list>
<mat-divider *ngIf="showAddresses"></mat-divider>
<app-address [showTitle]="showAddresses" buttonText="PROCEED TO NEXT STEP" checkboxText="Ship to the identical deal with" (isCheckboxChecked)="setSameShippingAddressAsBilling($occasion)" (createAddress)="updateBillingAddress($occasion)"></app-address>

That is what the billing deal with web page will seem like.

Screenshot of billing address page

Screenshot of billing deal with web page. (Large preview)

Transport Tackle Part

The transport deal with part behaves lots just like the billing deal with part. Nevertheless, there are a few variations. For one, the textual content displayed on the template is completely different. The opposite key variations are in how the order is up to date utilizing the OrderService as soon as an deal with is created or chosen. The fields that the order updates are shippingAddressCloneId for chosen addresses and shippingAddress for brand spanking new addresses. If a person chooses to alter the billing deal with, to be the identical because the transport deal with, the billingAddressSameAsShipping subject is up to date.

After a transport deal with is chosen and the order is up to date, the person is routed to the transport strategies web page.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-shipping-address',
  templateUrl: './shipping-address.part.html',
  styleUrls: ['./shipping-address.component.css']
)
export class ShippingAddressComponent implements OnInit 
  showAddresses: boolean = false;
  sameBillingAddressAsShipping: boolean = false;
  selectedCustomerAddressId: string = '';

  constructor(
    non-public addresses: AddressService,
    non-public snackBar: MatSnackBar,
    non-public session: SessionService,
    non-public orders: OrderService,
    non-public cart: CartService,
    non-public router: Router,
    non-public customerAddresses: CustomerAddressService)  

  ngOnInit() 
    this.session.loggedInStatus
      .subscribe(
        standing => this.showAddresses = standing
      );
  

  updateShippingAddress(deal with: Tackle) 
    if (this.showAddresses && this.selectedCustomerAddressId) 
      this.cloneAddress();
     else if (deal with.firstName && deal with.lastName && deal with.line1 && deal with.metropolis && deal with.zipCode && deal with.stateCode && deal with.countryCode && deal with.cellphone) 
      this.createAddress(deal with);
    
    else 
      this.snackBar.open('Examine your deal with. Some fields are lacking.', 'Shut');
    
  

  setCustomerAddress(customerAddressId: string) 
    this.selectedCustomerAddressId = customerAddressId;
  

  setSameBillingAddressAsShipping(change: boolean) 
    this.sameBillingAddressAsShipping = change;
  

  non-public createAddress(deal with: Tackle) 
    this.addresses.createAddress(deal with)
      .pipe(
        concatMap(
          deal with => 
            const replace = this.updateOrderObservable(
              id: this.cart.orderId,
              shippingAddressId: deal with.id
            , [UpdateOrderParams.shippingAddress]);

            if (this.showAddresses)  '', '')]);
             else 
              return replace;
            
          ))
      .subscribe(
        () => this.showSuccessSnackBar(),
        err => this.showErrorSnackBar()
      );
  

  non-public cloneAddress() 
    this.updateOrderObservable(
      id: this.cart.orderId,
      shippingAddressCloneId: this.selectedCustomerAddressId
    , [UpdateOrderParams.shippingAddressClone])
      .subscribe(
        () => this.showSuccessSnackBar(),
        err => this.showErrorSnackBar()
      );
  

  non-public updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> 
    return iif(() => this.sameBillingAddressAsShipping,
      concat([
        this.orders.updateOrder(order, updateParams),
        this.orders.updateOrder(order, [UpdateOrderParams.billingAddressSameAsShipping])
      ]),
      this.orders.updateOrder(order, updateParams)
    );
  

  non-public showErrorSnackBar() 
    this.snackBar.open('There was an issue creating your deal with.', 'Shut',  length: 8000 );
  

  non-public showSuccessSnackBar() 
    this.snackBar.open('Transport deal with efficiently added. Redirecting...', 'Shut',  length: 3000 );

    setTimeout(() => this.router.navigateByUrl('/shipping-methods'), 4000);
  

Right here is the template and its styling may be discovered here.

<app-title no="3" title="Transport Tackle" subtitle="Tackle to ship package deal to"></app-title>
<app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($occasion)"></app-address-list>
<mat-divider *ngIf="showAddresses"></mat-divider>
<app-address [showTitle]="showAddresses" buttonText="PROCEED TO SHIPPING METHODS" checkboxText="Invoice to the identical deal with" (isCheckboxChecked)="setSameBillingAddressAsShipping($occasion)" (createAddress)="updateShippingAddress($occasion)"></app-address>

The transport deal with web page will seem like this.

Screenshot of shipping address page

Screenshot of transport deal with web page. (Large preview)

Transport Strategies Part

This part shows the variety of shipments required for an order to be fulfilled, the obtainable transport strategies, and their related prices. The client can then choose a transport technique they like for every cargo.

The shipments property incorporates all of the shipments of the order. The shipmentsForm is the shape inside which the transport technique picks will likely be made.

When the part is initialized, the order is fetched and can comprise each its line gadgets and shipments. On the identical time, we get the supply lead instances for the assorted transport strategies. We use the OrderService to get the order and the DeliveryLeadTimeService for the lead instances. As soon as each units of data are returned, they’re mixed into an array of shipments and assigned to the shipments property. Every cargo will comprise its gadgets, the transport strategies obtainable, and the corresponding price.

After the person has chosen a transport technique for every cargo, the chosen transport technique is up to date for every in setShipmentMethods. If profitable, the person is routed to the funds web page.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-shipping-methods',
  templateUrl: './shipping-methods.part.html',
  styleUrls: ['./shipping-methods.component.css']
)
export class ShippingMethodsComponent implements OnInit {
  shipments: Cargo[] | undefined = [];
  shipmentsForm: FormGroup = this.fb.group();

  constructor(
    non-public orders: OrderService,
    non-public dlts: DeliveryLeadTimeService,
    non-public cart: CartService,
    non-public router: Router,
    non-public fb: FormBuilder,
    non-public shipments: ShipmentService,
    non-public snackBar: MatSnackBar
  )  

  ngOnInit() 
    combineLatest([
      this.orders.getOrder(this.cart.orderId, GetOrderParams.shipments),
      this.dlts.getDeliveryLeadTimes()
    ]).subscribe(
      ([lineItems, deliveryLeadTimes]) => 
        let li: LineItem;
        let lt: DeliveryLeadTime[];

        this.shipments = lineItems.shipments?.map((cargo) => 
          if (cargo.id) 
            this.shipmentsForm.addControl(cargo.id, new FormControl('', Validators.required));
          

          if (cargo.lineItems) 
            cargo.lineItems = cargo.lineItems.map(merchandise => 
              li = this.findItem(lineItems, merchandise.skuCode );
          

          if (cargo.availableShippingMethods) 
            lt = this.findLocationLeadTime(deliveryLeadTimes, cargo);
            cargo.availableShippingMethods = cargo.availableShippingMethods?.map(
              technique => 
                technique.deliveryLeadTime = this.findMethodLeadTime(lt, technique);
                return technique;
              );
          

          return cargo;
        );
      ,
      err => this.router.navigateByUrl('/error')
    );
  

  setShipmentMethods() 
    const shipmentsFormValue = this.shipmentsForm.worth;

    combineLatest(Object.keys(shipmentsFormValue).map(
      key => this.shipments.updateShipment(key, shipmentsFormValue[key])
    )).subscribe(
      () => 
        this.snackBar.open('Your shipments have been up to date with a transport technique.', 'Shut',  length: 3000 );
        setTimeout(() => this.router.navigateByUrl('/fee'), 4000);
      ,
      err => this.snackBar.open('There was an issue including transport strategies to your shipments.', 'Shut',  length: 5000 )
    );
  


  non-public findItem(lineItems: LineItem[], skuCode: string): LineItem 
    return lineItems.filter((merchandise) => merchandise.skuCode == skuCode)[0];
  

  non-public findLocationLeadTime(instances: DeliveryLeadTime[], cargo: Cargo): DeliveryLeadTime[] 
    return instances.filter((dlTime) => dlTime?.stockLocation?.id == cargo?.stockLocation?.id);
  

  non-public findMethodLeadTime(instances: DeliveryLeadTime[], technique: ShippingMethod): DeliveryLeadTime 
    return instances.filter((dlTime) => dlTime?.shippingMethod?.id == technique?.id)[0];
  
}

Right here is the template and you’ll find the styling at this link.

<kind id="container" [formGroup]="shipmentsForm">
    <app-title no="4" title="Transport Strategies" subtitle=" ship your packages"></app-title>
    <div class="shipment-container" *ngFor="let cargo of shipments; let j = index; let isLast = final">
        <h1>Cargo j+1 of shipments?.size</h1>
        <div class="row" *ngFor="let merchandise of cargo.lineItems">
            <img class="image-xs" [src]="merchandise.imageUrl" alt="product picture">
            <div id="shipment-details">
                <h4 id="item-name">merchandise.title</h4>
                <p>merchandise.skuCode</p>
            </div>
            <div id="quantity-section">
                <p id="quantity-label">Amount: </p>merchandise.amount
            </div>
        </div>
        <mat-radio-group [formControlName]="cargo?.id || j">
            <mat-radio-button *ngFor="let technique of cargo.availableShippingMethods" [value]="technique.id">
                <div class="radio-button">
                    <p>technique.title</p>
                    <div>
                        <p class="radio-label">Value:</p>
                        <p> technique.formattedPriceAmount</p>
                    </div>
                    <div>
                        <p class="radio-label">Timeline:</p>
                        <p> Obtainable in technique.deliveryLeadTime?.minDays-method.deliveryLeadTime?.maxDays days</p>
                    </div>
                </div>
            </mat-radio-button>
        </mat-radio-group>
        <mat-divider *ngIf="!isLast"></mat-divider>
    </div>
    <button mat-flat-button shade="main" [disabled]="shipmentsForm.invalid" (click on)="setShipmentMethods()">PROCEED TO PAYMENT</button>
</kind>

This can be a screenshot of the transport strategies web page.

Screenshot of shipping methods page

Screenshot of transport strategies web page. (Large preview)

Funds Part

On this part, the person clicks the fee button in the event that they want to proceed to pay for his or her order with Paypal. The approvalUrl is the Paypal hyperlink that the person is directed to after they click on the button.

Throughout initialization, we get the order with the fee supply included utilizing the OrderService. If a fee supply is ready, we get its id and retrieve the corresponding Paypal fee from the PaypalPaymentService. The Paypal fee will comprise the approval url. If no fee supply has been set, we replace the order with Paypal as the popular fee technique. We then proceed to create a brand new Paypal fee for the order utilizing the PaypalPaymentService. From right here, we will get the approval url from the newly created order.

Lastly, when the person clicks the button, they’re redirected to Paypal the place they’ll approve the acquisition.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-payment',
  templateUrl: './fee.part.html',
  styleUrls: ['./payment.component.css']
)
export class PaymentComponent implements OnInit 
  approvalUrl: string = '';

  constructor(
    non-public orders: OrderService,
    non-public cart: CartService,
    non-public router: Router,
    non-public funds: PaypalPaymentService
  )  

  ngOnInit() 
    const orderId = this.cart.orderId;

    this.orders.getOrder(orderId, GetOrderParams.paymentSource)
      .pipe(
        concatMap((order: Order) => ))
      .subscribe(
        paypalPayment => this.approvalUrl = paypalPayment?.approvalUrl 

  navigateToPaypal() 
    window.location.href = this.approvalUrl;
  

Right here is its template.

<app-simple-page quantity="5" title="Cost" subtitle="Pay to your order" buttonText="PROCEED TO PAY WITH PAYPAL" icon="point_of_sale" (buttonEvent)="navigateToPaypal()" [buttonDisabled]="approvalUrl.size ? false : true"></app-simple-page>

Right here’s what the funds web page will seem like.

Screenshot of payment page

Screenshot of fee web page. (Large preview)

Cancel Cost Part

Paypal requires a cancel fee web page. This part serves this objective. That is its template.

<app-simple-page title="Cost cancelled" subtitle="Your Paypal fee has been cancelled" icon="money_off" buttonText="GO TO HOME" [centerText]="true" route="https://smashingmagazine.com/"></app-simple-page>

Right here’s a screenshot of the web page.

Screenshot of payment cancellation page

Screenshot of fee cancellation web page. (Large preview)

Place Order Part

That is the final step within the checkout course of. Right here the person confirms that they certainly need to place the order and start its processing. When the person approves the Paypal fee, that is the web page they’re redirected to. Paypal provides a payer id question parameter to the url. That is the person’s Paypal Id.

When the part is initialized, we get the payerId question parameter from the url. The order is then retrieved utilizing the OrderService with the fee supply included. The id of the included fee supply is used to replace the Paypal fee with the payer id, utilizing the PaypalPayment service. If any of those fail, the person is redirected to the error web page. We use the disableButton property to stop the person from inserting the order till the payer Id is ready.

After they click on the place-order button, the order is up to date with a positioned standing. Afterwhich the cart is cleared, a profitable snack bar is displayed, and the person is redirected to the house web page.

@UntilDestroy( checkProperties: true )
@Part(
  selector: 'app-place-order',
  templateUrl: './place-order.part.html',
  styleUrls: ['./place-order.component.css']
)
export class PlaceOrderComponent implements OnInit 
  disableButton = true;

  constructor(
    non-public route: ActivatedRoute,
    non-public router: Router,
    non-public funds: PaypalPaymentService,
    non-public orders: OrderService,
    non-public cart: CartService,
    non-public snackBar: MatSnackBar
  )  

  ngOnInit() 
    this.route.queryParams
      .pipe(
        concatMap(params => 
          const payerId = params['PayerID'];
          const orderId = this.cart.orderId;

          return iif(
            () => payerId.size > 0,
            this.orders.getOrder(orderId, GetOrderParams.paymentSource)
              .pipe(
                concatMap(order =>  '';

                  return iif(
                    () => paymentSourceId ? paymentSourceId.size > 0 : false,
                    this.funds.updatePaypalPayment(paymentSourceId, payerId)
                  );
                )
              )
          );
        ))
      .subscribe(
        () => this.disableButton = false,
        () => this.router.navigateByUrl('/error')
      );
  

  placeOrder() 
    this.disableButton = true;

    this.orders.updateOrder(
      id: this.cart.orderId,
      place: true
    , [UpdateOrderParams.place])
      .subscribe(
        () => 
          this.snackBar.open('Your order has been efficiently positioned.', 'Shut',  length: 3000 );
          this.cart.clearCart();
          setTimeout(() => this.router.navigateByUrl("https://smashingmagazine.com/"), 4000);
        ,
        () => 
          this.snackBar.open('There was an issue inserting your order.', 'Shut',  length: 8000 );
          this.disableButton = false;
        
      );
  

Right here is the template and its associated styling.

<app-simple-page title="Finalize Order" subtitle="Full your order" [number]="'6'" icon="shopping_bag" buttonText="PLACE YOUR ORDER" (buttonEvent)="placeOrder()" [buttonDisabled]="disableButton"></app-simple-page>

Here’s a screenshot of the web page.

Screenshot of order placement page

Screenshot of order placement web page. (Large preview)

App Module

All requests made to Commerce Layer, apart from for authentication, have to comprise a token. So the second the app is initialized, a token is fetched from the /oauth/token route on the server and a session is initialized. We’ll use the APP_INITIALIZER token to supply an initialization operate through which the token is retrieved. Moreover, we’ll use the HTTP_INTERCEPTORS token to supply the OptionsInterceptor we created earlier. As soon as all of the modules are added the app module file ought to look one thing like this.

@NgModule(
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    BrowserAnimationsModule,
    AuthModule,
    ProductsModule,
    CartModule,
    CheckoutModule,
    CoreModule
  ],
  suppliers: [
    
      provide: HTTP_INTERCEPTORS,
      useClass: OptionsInterceptor,
      multi: true
    ,
    
      provide: APP_INITIALIZER,
      useFactory: (http: HttpClient) => () => http.post<object>(
        `$environment.apiUrl/oauth/token`,
         'grantType': 'client_credentials' ,
         withCredentials: true ),
      multi: true,
      deps: [HttpClient]
    
  ],
  bootstrap: [AppComponent]
)
export class AppModule  

App Part

We’ll modify the app part template and its styling which you’ll find here.

<div id="web page">
    <app-header></app-header>
    <div id="content material">
        <router-outlet></router-outlet>
    </div>
</div>

Conclusion

On this article, we’ve lined how you might create an e-commerce Angular 11 app with Commerce Layer and Paypal. We’ve additionally touched on easy methods to construction the app and the way you might interface with an e-commerce API.

Though this app permits a buyer to make an entire order, it isn’t by any means completed. There may be a lot you might add to enhance it. For one, it’s possible you’ll select to allow merchandise amount adjustments within the cart, hyperlink cart gadgets to their product pages, optimize the deal with elements, add extra guards for checkout pages just like the place-order web page, and so forth. That is simply the start line.

When you’d like to grasp extra in regards to the course of of creating an order from begin to end, you might take a look at the Commerce Layer guides and API. You’ll be able to view the code for this challenge at this repository.

Smashing Editorial(vf, yk, il)

Source link

Translate »