Quick summary ↬

Having an e-commerce retailer is crucial for any retailer proprietor as an rising variety of prospects are turning to on-line shopping for. On this tutorial, we’ll bear straightforward strategies to create an e-commerce web page with Angular 11. The situation will use the Commerce Layer as a headless e-commerce API and use Paypal to course of funds.

As of late it’s necessary to have an online primarily based presence when working a enterprise. Rather more shopping for is accomplished on-line than in earlier years. Having an e-commerce retailer permits retailer homeowners to open up completely different streams of revenue they couldn’t benefit from with solely a brick and mortar retailer. Totally different retailer homeowners nonetheless, run their firms on-line absolutely with out a bodily presence. This makes having an online primarily based retailer important.

Web sites resembling Etsy, Shopify and Amazon make it easy to rearrange a retailer pretty shortly with out having to worry about making a web page. Nonetheless, there is also instances the place retailer homeowners might need a personalized experience or probably save on the worth of proudly proudly owning a retailer on a couple of of those platforms.

Headless e-commerce API platforms current backends that retailer web sites can interface with. They deal with all processes and data related to the store like purchaser, orders, shipments, funds, and so forth. All that’s wished is a frontend to work along with this information. This offers homeowners a number of flexibility with reference to deciding how their prospects will experience their on-line retailer and the way in which they choose to run it.

On this text, we’ll cowl straightforward strategies to assemble an e-commerce retailer using Angular 11. We’ll use Commerce Layer as our headless e-commerce API. Although there is also tonnes of find out how to course of funds, we’ll exhibit straightforward strategies to make use of just one, Paypal.

Stipulations

Sooner than developing the app, you would possibly need to have Angular CLI put in. We’ll use it to initialize and scaffold the app. While you don’t have it put in however, you’ll get it through npm.

npm arrange -g @angular/cli

You’ll moreover desire a Commerce Layer developer account. Using the developer account, you need to to create a check out group and seed it with check out data. Seeding makes it less complicated to develop the app first with out worrying about what data you’ll must make use of. You’ll be capable of create an account at this link and an organization 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

Study the Seed with check out data discipline when making a model new group. (Large preview)

Lastly, you need to a Paypal Sandbox account. Having any such account will allow us to verify transactions between firms and prospects with out risking exact money. You’ll be capable of create one here. A sandbox account has a check out enterprise and try personal account already created for it.

Further after soar! Proceed learning beneath ↓

Commerce Layer And Paypal Config

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

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

The place to go looking 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’ll get the REST API shopper Id and secret. (Large preview)

To affiliate your Paypal enterprise account collectively together with your Commerce Layer group, go to your group’s dashboard. Proper right here you’ll add a Paypal payment gateway and a Paypal payment method to your different markets. Under Settings > Funds, select Value Gateways > Paypal and add your Paypal shopper 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 need to to create a Paypal payment method for each market you could be specializing in to make Paypal obtainable as an alternative. You’ll do this beneath Settings > Funds > Value Methods > New Value Method.

Payments Methods dashboard on Commerce Layer

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

A Bear in mind About Routes Used

Commerce Layer offers a route for authentication and one different fully 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 rest of the API routes take the pattern /api/:helpful useful resource.

The scope of this textual content solely covers the frontend portion of this app. I opted to retailer the tokens server side, use lessons to hint possession, and provide http-only cookies with a session id to the buyer. This received’t be lined proper right here because it’s exterior the scope of this textual content. Nonetheless, the routes keep the equivalent and exactly correspond to the Commerce Layer API. Although, there are a couple of personalized routes not obtainable from the Commerce Layer API that we’ll use. These primarily care for session administration. I’ll degree these out as we get to them and describe how one can receive the identical consequence.

One different inconsistency it’s potential you’ll uncover is that the request our our bodies differ from what the Commerce Layer API requires. Given that requests are handed on to a unique server to get populated with a token, I structured the our our bodies in one other method. This was to make it less complicated to ship requests. At any time when there are any inconsistencies throughout the request our our bodies, these will possible be recognized throughout the firms.

Since that’s out of scope, you’ll have to find out straightforward strategies to retailer tokens securely. You’ll moreover must barely modify request our our bodies to match exactly what the Commerce Layer API requires. When there could also be an inconsistency, I’ll hyperlink to the API reference and guides detailing straightforward strategies to appropriately building the physique.

App Development

To rearrange the app, we’ll break it down into Four necessary elements. A larger description of what each of the modules does is given beneath their corresponding sections:

  1. the core module,
  2. the knowledge module,
  3. the shared module,
  4. the perform modules.

The perform modules will group related pages and components collectively. There’ll possible be Four perform modules:

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

As we get to each module, I’ll make clear what its goal is and break down its contents.

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

src
├── app
│   ├── core
│   ├── data
│   ├── choices
│   │   ├── auth
│   │   ├── cart
│   │   ├── checkout
│   │   └── merchandise
└── shared

Producing The App And Together with Dependencies

We’ll begin by producing the app. Our group will possible be generally known as The LIme Mannequin and will have check out data already seeded by Commerce Layer.

ng new lime-app

We’ll need a couple of dependencies. Primarily Angular Material and Until Destroy. Angular Supplies will current components and styling. Until Destroy routinely unsubscribes from observables when components are destroyed. To place in them run:

npm arrange @ngneat/until-destroy
ng add @angular/supplies

Belongings

When together with addresses to Commerce Layer, an alpha-2 nation code have to be used. We’ll add a json file containing these codes to the property folder at property/json/country-codes.json. You possibly can discover this file linked here.

Varieties

The weather we’ll create share some world styling. We’ll place them in sorts.css which can be found at this link.

Environment

Our configuration will embrace two fields. The apiUrl which should degree to the Commerce Layer API. apiUrl is utilized by the businesses we’ll create to fetch data. The clientUrl should be the world the app is engaged on. We use this when setting redirect URLs for Paypal. You possibly can discover this file at this link.

Shared Module

The shared module will comprise firms, pipes, and components shared all through the other modules.

ng g m shared

It consists of three components, one pipe, and two firms. Proper right here’s what that will appear to be.

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

We’ll moreover use the shared module to export some typically used Angular Supplies components. This makes it less complicated to utilize them out of the sector as an alternative of importing each half all through different modules. Proper 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  

Components

Merchandise Quantity Half

This half models the quantity of issues when together with them to the cart. It’ll be used throughout the cart and merchandise modules. A material selector would have been a easy various for this goal. Nonetheless, the kind of the material select didn’t match the material inputs utilized in all the other sorts. A material menu appeared just like the material inputs used. So I decided to create a select half with it as an alternative.

ng g c shared/components/item-quantity

The half might have three enter properties and one output property. quantity models the preliminary quantity of issues, maxValue signifies the utmost number of devices that could be chosen in a single go, and disabled signifies whether or not or not the half should be disabled or not. The setQuantityEvent is triggered when a quantity is chosen.

When the half is initialized, we’ll set the values that appear on the material menu. There moreover exists a method generally known as setQuantity that will emit setQuantityEvent events.

This is the half file.

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

  values: amount[] = [];

  constructor()  

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

  setQuantity(price: amount) 
    this.setQuantityEvent.emit(price);
  

That’s its template.

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

Proper right here is its styling.

button 
    margin: 3px;

Title Half

This half doubles as a stepper title along with a plain title on some easier pages. Although Angular Supplies offers a stepper half, it wasn’t the best match for a fairly prolonged checkout course of, wasn’t as responsive on smaller reveals, and required way more time to implement. A easier title nonetheless might very effectively be repurposed as a stepper indicator and be useful all through a lot of pages.

ng g c shared/components/title

The half has Four enter properties: a title, a subtitle, a amount (no), and centerText, to level whether or not or to not coronary heart the textual content material of the half.

@Half(
  selector: 'app-title',
  templateUrl: './title.half.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 possibly can discover 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>

Straightforward Net web page Half

There are a variety of instances the place a title, an icon, and a button had been all that had been wished for an online web page. These embody a 404 net web page, an empty cart net web page, an error net web page, a payment net web page, and an order placement net web page. That’s the goal the easy net web page half will serve. When the button on the net web page is clicked, it ought to each redirect to a route or perform some movement in response to a buttonEvent.

To make it:

ng g c shared/components/simple-page

This is its half file.

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

  constructor(private 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="amount" title="title" subtitle="subtitle" [centerText]="centerText"></app-title>
    <div *ngIf="icon" id="icon-container">
        <mat-icon shade="most important" class="icon">icon</mat-icon>
    </div>
    <button mat-flat-button shade="most important" (click on on)="buttonClicked()" [disabled]="buttonDisabled">
        buttonText
    </button>
</div>

It’s styling could also be found here.

Pipes

Phrase Wrap Pipe

Some merchandise’ names and differing types of information displayed on the positioning are literally prolonged. In some instances, getting these prolonged sentences to wrap in supplies components is tough. So we’ll use this pipe to cut the sentences proper all the way down to a specified dimension and add ellipses to the tip of the consequence.

To create it run:

ng g pipe shared/pipes/word-wrap

It might probably comprise:

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

@Pipe(
  title: 'wordWrap'
)
export class WordWrapPipe implements PipeTransform 
  transform(price: string, dimension: amount): string 
    return `$price.substring(0, dimension)...`;
  

Suppliers

HTTP Error Handler Service

There are pretty numerous http firms on this problem. Creating an error handler for each method is repetitive. So creating one single handler that may be utilized by all methods is sensible. The error handler will be utilized to format an error and likewise cross on the errors to completely different exterior logging platforms.

Generate it by working:

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

This service will comprise only one method. The tactic will format the error message to be displayed counting on whether or not or not it’s a shopper or server error. Nonetheless, there could also be room to reinforce it further.

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

  constructor()  

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

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

    return throwError(displayMessage);
  

Native Storage Service

We’ll use native storage to take care of monitor of the number of devices in a cart. It’s moreover useful to retailer the Id of an order proper right here. An order corresponds to a cart on Commerce Layer.

To generate the native storage service run:

ng g s shared/firms/local-storage

The service will comprise Four methods in order so as to add, delete, and get devices from native storage and one different to clear it.

import  Injectable  from '@angular/core';

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

  constructor()  

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

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

  getItem(key: string): string 

Data Module

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

src/app/data
├── data.module.ts
├── fashions
└── firms

To generate the module run:

ng g m data

Fashions

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

for model in 
take care of cart nation customer-address 
purchaser delivery-lead-time line-item order 
payment-method payment-source paypal-payment 
worth cargo shipping-method sku stock-location; 
do ng g interface "data/fashions/$model"; executed

The following desk hyperlinks to each file and offers an overview of what each interface is.

Interface Description
Address Represents a standard take care of.
Cart Shopper side mannequin of an order monitoring the number of merchandise a purchaser intends to purchase.
Country Alpha-2 nation code.
Customer Address An take care of associated to a purchaser.
Customer A registered individual.
Delivery Lead Time Represents the time frame it ought to take to provide a cargo.
Line Item An itemized product added to the cart.
Order A shopping for cart or assortment of line devices.
Payment Method A payment kind made obtainable to an order.
Payment Source A payment associated to an order.
Paypal Payment A payment made through Paypal
Price Worth associated to an SKU.
Shipment Assortment of issues shipped collectively.
Shipping Method Method through which a package deal deal is shipped.
SKU A singular stock-keeping unit.
Stock Location Location that comes with SKU inventory.

Suppliers

This folder incorporates the businesses that create, retrieve, and manipulate app data. We’ll create 11 firms proper right here.

for service in 
take care of cart nation customer-address 
purchaser delivery-lead-time line-item 
order paypal-payment cargo sku; 
do ng g s "data/firms/$service"; executed

Sort out Service

This service creates and retrieves addresses. It’s vital when creating and assigning transport and billing addresses to orders. It has two methods. One to create an take care of and one different to retrieve one.

The route used proper right here is /api/addresses. While you’re going to utilize the Commerce Layer API straight, you’ll want to building the knowledge as demonstrated in this example.

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

  constructor(private http: HttpClient, private eh: HttpErrorHandler)  

  createAddress(take care of: Sort out): Observable<Sort out> 
    return this.http.submit<Sort out>(this.url, take care of)
      .pipe(catchError(this.eh.handleError));
  

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

Cart Service

The cart is accountable for sustaining the quantity of issues added and the order Id. Making API calls to get the number of devices in an order everytime a model new line merchandise is created could also be expensive. As an alternative, we might merely use native storage to maintain up the depend on the buyer. This eliminates the need to make pointless order fetches every time an merchandise is added to the cart.

We moreover use this service to retailer the order Id. A cart corresponds to an order on Commerce Layer. As quickly as the first merchandise is added to the cart, an order is created. We’ve got to guard this order Id so we’ll fetch it by the checkout course of.

Furthermore, we’d like a choice to speak to the header that an merchandise has been added to the cart. The header incorporates the cart button and reveals the amount of issues in it. We’ll use an observable of a BehaviorSubject with the current price of the cart. The header can subscribe to this and monitor changes throughout the cart price.

Lastly, as quickly as an order has been completed the cart price have to be cleared. This ensures that there’s no confusion when creating subsequent newer orders. The values that had been saved are cleared as quickly as the current order is marked as positioned.

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

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

  cartValue$ = this.cart.asObservable();

  constructor(private 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(): amount 
    const itemCount = this.storage.getItem('item-count');

    return itemCount ? parseInt(itemCount) : 0;
  

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

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

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

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

Nation Service

When together with addresses on Commerce Layer, the nation code must be an alpha 2 code. This service reads a json file containing these codes for every nation and returns it in its getCountries method.

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

  constructor(private http: HttpClient)  

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

Purchaser Sort out Service

This service is used to affiliate addresses with prospects. It moreover fetches a specific or all addresses related to a purchaser. It’s used when the consumer offers their transport and billing addresses to their order. The createCustomer method creates a purchaser, getCustomerAddresses will get all of a purchaser’s addresses, and getCustomerAddress will get a specific one.

When making a purchaser take care of, make certain to building the submit physique consistent with this example.

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

  constructor(private http: HttpClient, private 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));
  

Purchaser Service

Shoppers are created and their information retrieved using this service. When an individual indicators up, they modify right into a purchaser and are created using the createCustomerMethod. getCustomer returns the consumer associated to a specific Id. getCurrentCustomer returns the consumer in the mean time logged in.

When making a purchaser, building the knowledge like this. You’ll be capable of add their first and ultimate names to the metadata, as confirmed in its attributes.

The route /api/prospects/current is simply not obtainable on Commerce Layer. So that you simply’ll need to decide straightforward strategies to get the in the mean time logged in purchaser.

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

  constructor(private http: HttpClient, private eh: HttpErrorHandler)  

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

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

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

Provide Lead Time Service

This service returns particulars about transport timelines from different stock locations.

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

  constructor(private http: HttpClient, private 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 could create an merchandise the second it’s added to the cart. An merchandise’s information may be fetched. The merchandise may also be updated when its quantity changes or deleted when away from the cart.

When creating devices or updating them, building the request physique as confirmed on this example.

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

  constructor(private http: HttpClient, private 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, quantity: amount): Observable<LineItem> 
    return this.http.patch<LineItem>(`$this.url/$id`,  quantity: quantity )
      .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

Identical to the street merchandise service, the order service means that you may create, change, delete, or get an order. Furthermore, it’s potential you’ll choose to get the shipments associated to an order individually using the getOrderShipments method. This service is used carefully all by the checkout course of.

There are fully differing types of particulars about an order which could be required all by checkout. Because it could be expensive to fetch a complete order and its relations, we specify what we have to get from an order using GetOrderParams. The equal of this on the CL API is the include query parameter the place you document the order relationships to be included. You’ll be capable of confirm what fields ought to be included for the cart summary here and for the numerous checkout phases here.

Within the equivalent methodology, when updating an order, we use UpdateOrderParams to specify change fields. It’s as a result of throughout the server that populates the token, some further operations are carried out counting on what topic is being updated. Nonetheless, must you’re making direct requests to the CL API, you don’t need to specify this. You’ll be able to eliminate it given that CL API doesn’t require you to specify them. Although, the request physique should resemble this example.

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

  constructor(
    private http: HttpClient,
    private 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:  'topic': 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 Value Service

This service is accountable for creating and updating Paypal funds for orders. Furthermore, we’ll get a Paypal payment given its id. The submit physique should have a building very similar to this example when making a Paypal payment.

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

  constructor(private http: HttpClient, private eh: HttpErrorHandler)  

  createPaypalPayment(payment: PaypalPayment): Observable<PaypalPayment> 
    return this.http.submit<PaypalPayment>(this.url, payment)
      .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 change should look very similar to this example.

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

  constructor(private http: HttpClient, private 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 store. If a lot of merchandise are being retrieved, they’re usually paginated and have an online web page dimension set. Net web page dimension and net web page amount should be set as query params like in this example must you’re making direct requests to the API. A single product may be retrieved given its id.

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

  constructor(private http: HttpClient, private eh: HttpErrorHandler)  

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

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

Core Module

The core module incorporates each little factor central to and customary all through the equipment. These embody components identical to the header and pages identical to the 404 net web page. Suppliers accountable for authentication and session administration moreover fall proper right here, along with app-wide interceptors and guards.

The core module tree will appear to be this.

src/app/core
├── components
│   ├── error
│   │   ├── error.half.css
│   │   ├── error.half.html
│   │   └── error.half.ts
│   ├── header
│   │   ├── header.half.css
│   │   ├── header.half.html
│   │   └── header.half.ts
│   └── not-found
│       ├── not-found.half.css
│       ├── not-found.half.html
│       └── not-found.half.ts
├── core.module.ts
├── guards
│   └── empty-cart.guard.ts
├── interceptors
│   └── decisions.interceptor.ts
└── firms
    ├── 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/decisions
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 love this. Bear in mind 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  

Suppliers

The businesses folder holds the authentication, session, and header firms.

Authentication Service

The AuthenticationService means that you may buy client and customer tokens. These tokens are used to entry the rest of the API’s routes. Purchaser tokens are returned when an individual exchanges an e mail and password for it and have a wider range of permissions. Shopper tokens are issued without having credentials and have narrower permissions.

getClientSession will get a shopper token. login will get a purchaser token. Every methods moreover create a session. The physique of a shopper token request should look like this and that of a purchaser token like this.

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

  constructor(private http: HttpClient, private 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 generally known as loggedInStatus to talk whether or not or not an individual is logged in. setLoggedInStatus models the value of this subject, true for logged in, and false for not logged in. isCustomerLoggedIn makes a request to the server to confirm if the individual has an current session. logout destroys the session on the server. The ultimate two methods entry routes which could be distinctive to the server that populates the request with a token. They don’t appear to be obtainable from Commerce Layer. You’ll have to find out straightforward strategies to implement them.

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

  loggedInStatus = this.isLoggedIn.asObservable();

  constructor(private http: HttpClient, private eh: HttpErrorHandler)  

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

  isCustomerLoggedIn(): Observable< message: string > 
    return this.http.get< message: string >(`$this.url/purchaser/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 talk whether or not or not the cart, login, and logout buttons should be confirmed throughout the header. These buttons are hidden on the login and signup pages nonetheless present on all completely different pages to cease confusion. We’ll use an observable from a BehaviourSubject generally known as showHeaderButtons that shares this. We’ll actually have a setHeaderButtonsVisibility method to set this price.

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

  showHeaderButtons = this.headerButtonsVisibility.asObservable();

  constructor()  

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

Components

Error Half

This half is used as an error net web page. It’s useful in instances when server requests fail and fully no data is displayed on an online web page. As an alternative of exhibiting a clear net web page, we let the individual know that a difficulty occurred. Beneath is it’s template.

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

That’s what the half will appear to be.

Screenshot of error page

Screenshot of error net web page. (Large preview)

Not Found Half

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

<app-simple-page title="404: Net web page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested net web page could not be found" [centerText]="true" route="https://smashingmagazine.com/"></app-simple-page>

Screenshot of 404 page

Screenshot of 404 net web page. (Large preview)

The HeaderComponent is especially the header displayed on the prime of an online web page. It might probably comprise the app title, the cart, login, and logout buttons.

When this half is initialized, a request is made to confirm whether or not or not the individual has a gift session. This happens when subscribing to this.session.isCustomerLoggedIn(). We subscribe to this.session.loggedInStatus to confirm if the individual logs out all by the lifetime of the app. The this.header.showHeaderButtons subscription decides whether or not or to not point out all the buttons on the header or disguise them. this.cart.cartValue$ will get the rely of issues throughout the cart.

There exists a logout method that destroys an individual’s session and assigns them a shopper token. A shopper token is assigned on account of the session sustaining their purchaser token is destroyed and a token stays to be required for each API request. A material snackbar communicates to the individual whether or not or not their session was effectively destroyed or not.

We use the @UntilDestroy( checkProperties: true ) decorator to level that every one subscriptions should be routinely unsubscribed from when the half is destroyed.

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

  constructor(
    private session: SessionService,
    private snackBar: MatSnackBar,
    private cart: CartService,
    private header: HeaderService,
    private 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 got gotten been logged out.', 'Shut',  size: 4000 );
        this.session.setLoggedInStatus(false);
      ,
      err => this.snackBar.open('There was a difficulty logging you out.', 'Shut',  size: 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="most important" aria-label="shopping for cart">
                <mat-icon [matBadge]="cartAmount" matBadgeColor="accent" aria-label="shopping for cart" routerLink="/cart">shopping_cart</mat-icon>
            </button>
            <button mat-icon-button shade="most important" aria-label="login" *ngIf="!isLoggedIn">
                <mat-icon aria-label="login" matTooltip="login" routerLink="/login">login</mat-icon>
            </button>
            <button mat-icon-button shade="most important" aria-label="logout" *ngIf="isLoggedIn" (click on on)="logout()">
                <mat-icon aria-label="logout" matTooltip="logout">logout</mat-icon>
            </button>
        </div>
    </div>
</div>

Guards

Empty Cart Guard

This guard prevents prospects from accessing routes relating to checkout and billing if their cart is empty. It’s as a result of to proceed with checkout, there have to be a authentic order. An order corresponds to a cart with devices in it. If there are devices throughout the cart, the individual can proceed to a guarded net web page. Nonetheless, if the cart is empty, the individual is redirected to an empty-cart net web page.

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

Interceptors

Selections Interceptor

This interceptor intercepts all outgoing HTTP requests and offers two decisions to the request. These are a Content material material-Kind header and a withCredentials property. withCredentials specifies whether or not or not a request should be despatched with outgoing credentials identical to the http-only cookies that we use. We use Content material material-Kind to level that we’re sending json belongings 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 material-Kind', 'utility/json'),
      withCredentials: true
    );

    return subsequent.take care of(request);
  

Perform Modules

This half incorporates the first choices of the app. As talked about earlier, the choices are grouped in Four modules: auth, product, cart, and checkout modules.

Merchandise Module

The merchandise module incorporates pages that present merchandise on sale. These embody the product net web page and the product document net web page. It’s structured as confirmed beneath.

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

To generate it and its components:

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

That’s 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 Guidelines Half

This half reveals a paginated document of obtainable merchandise in the marketplace. It’s the primary net web page that’s loaded when the app begins.

The merchandise are displayed in a grid. Supplies grid document is the best half for this. To make the grid responsive, the number of grid columns will change counting on the show dimension. The BreakpointObserver service permits us to seek out out the dimensions of the show and assign the columns all through initialization.

To get the merchandise, we identify the getProducts strategy of the SkuService. It returns the merchandise if worthwhile and assigns them to the grid. If not, we route the individual to the error net web page.

Given that merchandise displayed are paginated, we might have a getNextPage method to get the additional merchandise.

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

The template is confirmed beneath and its styling could also be found 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 image">
            <mat-card-content>
                <mat-card-title matTooltip="product.title">product.title </mat-card-title>
                <mat-card-subtitle>product.prices[0].compareAtAmountFloat </mat-card-subtitle>
            </mat-card-content>
            <mat-card-actions>
                <button mat-flat-button shade="most important" [routerLink]="['/product', product.id]">
                    View
                </button>
            </mat-card-actions>
        </mat-card>
    </mat-grid-tile>
</mat-grid-list>
<mat-paginator [length]="dimension" [pageIndex]="pageIndex" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" (net web page)="pageEvent = getNextPage($event)">
</mat-paginator>

The net web page will appear to be this.

Screenshot of product list page

Screenshot of product listpage. (Large preview)

Product Half

As quickly as a product is chosen from the product document net web page, this half reveals its particulars. These embody the product’s full title, worth, and description. There’s moreover a button in order so as to add the merchandise to the product cart.

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

When the individual offers an merchandise to the cart, the addItemToCart method is called. In it, we confirm if an order has already been created for the cart. If not, a model new one is made using the OrderService. Afterwhich, a line merchandise is created throughout the order that corresponds to the product. If an order already exists for the cart, merely the street merchandise is created. Counting on the standing of the requests, a snackbar message is flaunted to the individual.

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

  constructor(
    private route: ActivatedRoute,
    private skus: SkuService,
    private location: Location,
    private router: Router,
    private header: HeaderService,
    private orders: OrderService,
    private lineItems: LineItemService,
    private cart: CartService,
    private 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.quantity > 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,
                quantity: this.quantity,
                skuCode: this.product.code
              );
            )
          )
          .subscribe(
            () => 
              this.cart.incrementItemCount(this.quantity);
              this.showSuccessSnackBar();
            ,
            err => this.showErrorSnackBar()
          );
       else 
        this.lineItems.createLineItem(
          orderId: this.cart.orderId,
          title: this.product.title,
          imageUrl: this.product.imageUrl,
          quantity: this.quantity,
          skuCode: this.product.code
        ).subscribe(
          () => 
            this.cart.incrementItemCount(this.quantity);
            this.showSuccessSnackBar();
          ,
          err => this.showErrorSnackBar()
        );
      
     else 
      this.snackBar.open('Select a quantity increased than 0.', 'Shut',  size: 8000 );
    
  

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

  goBack() 
    this.location.once more();
  

  private showSuccessSnackBar() 
    this.snackBar.open('Merchandise effectively added to cart.', 'Shut',  size: 8000 );
  

  private showErrorSnackBar() 
    this.snackBar.open('Failed in order so as to add your merchandise to the cart.', 'Shut',  size: 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.prices[0].compareAtAmountFloat </mat-card-subtitle>
            <p>
                product.description
            </p>
        </mat-card-content>
        <mat-card-actions>
            <app-item-quantity [quantity]="quantity" [maxValue]="10" (setQuantityEvent)="setQuantity($event)"></app-item-quantity>
            <button mat-raised-button shade="accent" (click on on)="addItemToCart()">
                <mat-icon>add_shopping_cart</mat-icon>
                Add to cart
            </button>
            <button mat-raised-button shade="most important" (click on on)="goBack()">
                <mat-icon>storefront</mat-icon>
                Proceed shopping for
            </button>
        </mat-card-actions>
    </mat-card>
</div>

The net web page will appear to be this.

Screenshot of product page

Screenshot of product net 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/choices/auth/
├── auth.module.ts
└── pages
    ├── login
    │   ├── login.half.css
    │   ├── login.half.html
    │   └── login.half.ts
    └── signup
        ├── signup.half.css
        ├── signup.half.html
        └── signup.half.ts

To generate it and its components:

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

That’s 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 Half

An individual indicators up for an account using this half. A major title, ultimate title, e mail, and password are required for the strategy. The individual moreover needs to confirm their password. The enter fields will possible be created with the FormBuilder service. Validation is added to require that every one the inputs have values. Further validation is added to the password topic to verify a minimal dimension of eight characters. A personalized matchPasswords validator ensures that the confirmed password matches the preliminary password.

When the half is initialized, the cart, login, and logout buttons throughout the header are hidden.That’s communicated to the header using the HeaderService.

After all the fields are marked as authentic, the individual can then be part of. Inside the signup method, the createCustomer strategy of the CustomerService receives this enter. If the signup is worthwhile, the individual is educated that their account was effectively created using a snackbar. They’re then rerouted to the home net web page.

@UntilDestroy( checkProperties: true )
@Half(
  selector: 'app-signup',
  templateUrl: './signup.half.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.

<form id="container" [formGroup]="signupForm" (ngSubmit)="signup()">
    <h1 class="mat-display-3">Create Account</h1>
    <mat-form-field look="outline">
        <mat-label>First Title</mat-label>
        <enter matInput formControlName="firstName">
        <mat-icon matPrefix>portrait</mat-icon>
    </mat-form-field>
    <mat-form-field look="outline">
        <mat-label>Last Title</mat-label>
        <enter matInput formControlName="lastName">
        <mat-icon matPrefix>portrait</mat-icon>
    </mat-form-field>
    <mat-form-field look="outline">
        <mat-label>E mail</mat-label>
        <enter matInput formControlName="e mail" kind="e mail">
        <mat-icon matPrefix>alternate_email</mat-icon>
    </mat-form-field>
    <mat-form-field look="outline">
        <mat-label>Password</mat-label>
        <enter matInput formControlName="password" kind="password">
        <mat-icon matPrefix>vpn_key</mat-icon>
    </mat-form-field>
    <mat-form-field look="outline">
        <mat-label>Affirm Password</mat-label>
        <enter matInput formControlName="confirmedPassword" kind="password">
        <mat-icon matPrefix>vpn_key</mat-icon>
    </mat-form-field>
    <div *ngIf="confirmedPassword?.invalid && (confirmedPassword?.dirty || confirmedPassword?.touched)">
        <mat-error *ngIf="signupForm.hasError('differentPasswords')">
            Your passwords do not match.
        </mat-error>
    </div>
    <div *ngIf="password?.invalid && (password?.dirty || password?.touched)">
        <mat-error *ngIf="password?.hasError('minlength')">
            Your password should be on the very least eight characters.
        </mat-error>
    </div>
    <button mat-flat-button shade="most important" [disabled]="!signupForm.authentic">Sign Up</button>
</form>

The half will find yourself as follows.

Screenshot of signup page

Screenshot of signup net web page. (Large preview)

Login Half

A registered individual logs into their account with this half. An e mail and password ought to be entered. Their corresponding enter fields would have validation that makes them required.

Identical to the SignupComponent, the cart, login, and logout buttons throughout the header are hidden. Their visibility is prepared using the HeaderService all through half initialization.

To login, the credentials are handed to the AuthenticationService. If worthwhile, the login standing of the individual is prepared using the SessionService. The individual is then routed once more to the net web page that they had been on. If unsuccessful, a snackbar is displayed with an error and the password topic is reset.

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

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

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

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

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

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

Beneath is the LoginComponent template.

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

Right here’s a screenshot of the net web page.

Screenshot of login page

Screenshot of login net web page. (Large preview)

Cart Module

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

src/app/choices/cart/
├── cart.module.ts
└── pages
    ├── codes
    │   ├── codes.half.css
    │   ├── codes.half.html
    │   └── codes.half.ts
    ├── empty
    │   ├── empty.half.css
    │   ├── empty.half.html
    │   └── empty.half.ts
    └── summary
        ├── summary.half.css
        ├── summary.half.html
        └── summary.half.ts

To generate it, run:

ng g m choices/cart
ng g c choices/cart/codes
ng g c choices/cart/empty
ng g c choices/cart/summary

That’s the module file.

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

Codes Half

As talked about earlier, this half is used in order so as to add any coupon or reward card codes to an order. This permits the individual to make use of reductions to the entire of their order sooner than persevering with to checkout.

There’ll possible be two enter fields. One for coupons and one different for reward card codes.

The codes are added by updating the order. The updateOrder strategy of the OrderService updates the order with the codes. Afterwhich, every fields are reset and the individual is educated of the success of the operation with a snackbar. A snackbar may also be confirmed when an error occurs. Every the addCoupon and addGiftCard methods identify the updateOrder method.

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

  constructor(
    private cart: CartService,
    private order: OrderService,
    private snackBar: MatSnackBar
  )  

  private updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) 
    this.order.updateOrder(order, params)
      .subscribe(
        () => 
          this.snackBar.open(`Effectively added $codeType code.`, 'Shut',  size: 8000 );
          this.couponCode.reset();
          this.giftCardCode.reset();
          this.codesDirective?.reset();
        ,
        err => this.snackBar.open(`There was a difficulty together with your $codeType code.`, 'Shut',  size: 8000 )
      );
  

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

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

The template is confirmed beneath and its styling could also be found 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="outline">
            <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 on)="addCoupon()">Redeem</button>
    </div>
    <div class="input-row">
        <mat-form-field look="outline">
            <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 on)="addGiftCard()">Redeem</button>
    </div>
    <button shade="most important" mat-flat-button routerLink="/cart">
        <mat-icon>shopping_cart</mat-icon>
        CONTINUE TO CART
    </button>
</div>

Right here’s a screenshot of the net web page.

Screenshot of codes page

Screenshot of codes net web page. (Large preview)

Empty Half

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

It’s a fairly easy half that has some textual content material indicating to the individual that their cart is empty. It moreover has a button that the individual can click on on to go to the homepage in order so as to add points to their cart. So we’ll use the SimplePageComponent to indicate it. Proper right here is the template.

<app-simple-page title="Your cart is empty" subtitle="There could also be in the mean time nothing in your cart. Head to the home net web page in order so as to add devices." buttonText="GO TO HOME PAGE" icon="shopping_basket" [centerText]="true" route="https://smashingmagazine.com/">
</app-simple-page>

Right here’s a screenshot of the net web page.

Screenshot of empty cart page

Screenshot of empty cart net web page. (Large preview)

Summary Half

This half summarizes the cart/order. It lists all the devices throughout the cart, their names, parts, and photographs. It furthermore breaks down the worth of the order along with taxes, transport, and reductions. The individual should have the flexibility to view this and decide whether or not or not they’re pleased with the devices and value sooner than persevering with to checkout.

On initialization, the order and its line devices are fetched using the OrderService. An individual should have the flexibility to switch the street devices and even take away them from the order. Objects are eradicated when the deleteLineItem method is called. In it the deleteLineItem strategy of the LineItemService receives the id of the street merchandise to be deleted. If a deletion is worthwhile, we change the merchandise rely throughout the cart using the CartService.

The individual is then routed to the consumer net web page the place they begin the strategy of attempting out. The checkout method does the routing.

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

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

  constructor(
    private orders: OrderService,
    private lineItems: LineItemService,
    private cart: CartService,
    private snackBar: MatSnackBar,
    private router: Router
  )  

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

  private processOrder(order: Order) 
    this.order = order;

    this.summary = [
       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' 
    ];
  

  private showOrderError(msg: string) 
    this.snackBar.open(`There was a difficulty $msg.`, 'Shut',  size: 8000 );
  

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

  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.amount (order.skusCount devices)</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 image">
            <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.quantity || 0" [disabled]="true"></app-item-quantity>
            <div class="itemTotal"> merchandise.formattedTotalAmount  </div>
            <button mat-icon-button shade="warn" (click on 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 summary" [id]="merchandise.id">
            <h3 class="costLabel">merchandise.title</h3>
            <p> merchandise.amount  </p>
        </div>
    </div>
    <mat-divider></mat-divider>
    <div class="costSummary">
        <div class="costItem" id="complete">
            <h2 id="totalLabel">Full</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="most important" mat-flat-button (click on on)="checkout()">
        <mat-icon>point_of_sale</mat-icon>
        CHECKOUT
    </button>
    </div>
</div>

Right here’s a screenshot of the net web page.

Screenshot of summary page

Screenshot of summary net web page. (Large preview)

Checkout Module

This module is accountable for the checkout course of. Checkout entails providing a billing and transport take care of, a purchaser e mail, and deciding on a transport and payment method. The ultimate step of this course of is placement and affirmation of the order. The development of the module is as follows.

src/app/choices/checkout/
├── components
│   ├── take care of
│   ├── address-list
│   └── country-select
└── pages
    ├── billing-address
    ├── cancel-payment
    ├── purchaser
    ├── payment
    ├── place-order
    ├── shipping-address
    └── shipping-methods

This module is the most important by far and incorporates Three components and 7 pages. To generate it and its components run:

ng g m choices/checkout
for comp in 
take care of address-list country-select; do 
ng g c "choices/checkout/components/$comp" 
; executed
for net web page in 
billing-address cancel-payment purchaser 
payment place-order shipping-address 
shipping-methods; do 
ng g c "choices/checkout/pages/$net web page"; executed

That’s the module file.

@NgModule(
  declarations: [
    CustomerComponent,
    AddressComponent,
    BillingAddressComponent,
    ShippingAddressComponent,
    ShippingMethodsComponent,
    PaymentComponent,
    PlaceOrderComponent,
    AddressListComponent,
    CountrySelectComponent,
    CancelPaymentComponent
  ],
  imports: [
    RouterModule.forChild([
      
        path: '', canActivate: [EmptyCartGuard], kids: [
           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  

Components

Nation Select Half

This half lets an individual select a country as part of an take care of. The material select half has a reasonably fully completely different look when compared with the enter fields throughout the take care of form. So for the sake of uniformity, a material menu half is used as an alternative.

When the half is initialized, the nation code data is fetched using the CountryService. The worldwide places property holds the values returned by the service. These values will possible be added to the menu throughout the template.

The half has one output property, setCountryEvent. When a country is chosen, this event emits the alpha-2 code of the nation.

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

  constructor(private worldwide places: CountryService)  

  ngOnInit() 
    this.worldwide places.getCountries()
      .subscribe(
        worldwide places => 
          this.worldwide places = worldwide places;
        
      );
  

  setCountry(price: Nation) 
    this.nation = price.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 countries" (click on on)="setCountry(cnt)" mat-menu-item>cnt.title</button>
</mat-menu>

Sort out Half

This could be a form for capturing addresses. It’s utilized by every the transport and billing take care of pages. A authentic Commerce Layer take care of should comprise a major and ultimate title, an take care of line, a metropolis, zip code, state code, nation code, and cellphone amount.

The FormBuilder service will create the form group. Since this half is utilized by a lot of pages, it has numerous enter and output properties. The enter properties embody the button textual content material, title displayed, and textual content material for a checkbox. The output properties will possible be event emitters for when the button is clicked to create the take care of and one different for when the checkbox price changes.

When the button is clicked, the addAddress method is called and the createAddress event emits your entire take care of. Equally, when the checkbox is checked, the isCheckboxChecked event emits the checkbox price.

@Half(
  selector: 'app-address',
  templateUrl: './take care of.half.html',
  styleUrls: ['./address.component.css']
)
export class AddressComponent  undefined;

  constructor(private fb: FormBuilder)  

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

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

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

That’s its template and its styling is linked here.

<form id="container" [formGroup]="addressForm">
    <p class="mat-headline" *ngIf="showTitle">Or add a model new take care of</p>
    <div class="row">
        <mat-form-field look="outline">
            <mat-label>First Title</mat-label>
            <enter matInput formControlName="firstName">
        </mat-form-field>
        <mat-form-field look="outline">
            <mat-label>Last Title</mat-label>
            <enter matInput formControlName="lastName">
        </mat-form-field>
    </div>
    <div class="row">
        <mat-form-field look="outline">
            <mat-label>Sort out</mat-label>
            <enter matInput formControlName="line1">
        </mat-form-field>
        <mat-form-field look="outline">
            <mat-label>Metropolis</mat-label>
            <enter matInput formControlName="metropolis">
        </mat-form-field>
    </div>
    <div class="row">
        <mat-form-field look="outline">
            <mat-label>State Code</mat-label>
            <enter matInput formControlName="stateCode">
        </mat-form-field>
        <mat-form-field look="outline">
            <mat-label>Zip Code</mat-label>
            <enter matInput formControlName="zipCode">
        </mat-form-field>
    </div>
    <div class="row">
        <mat-form-field look="outline">
            <mat-label>Cellphone</mat-label>
            <enter matInput formControlName="cellphone">
        </mat-form-field>
        <app-country-select (setCountryEvent)="setCountryCode($event)"></app-country-select>
    </div>
    <mat-checkbox shade="accent" (change)="setCheckboxValue($event)">
        checkboxText
    </mat-checkbox>
    <button id="submit-button" mat-flat-button shade="most important" (click on on)="addAddress()">
        buttonText
    </button>
</form>

Sort out Guidelines Half

When a purchaser logs in, they’ll entry their current addresses. As an alternative of getting them re-enter an take care of, they’ll select from an take care of document. That’s the goal of this half. On initialization, all the consumer’s addresses are fetched using the CustomerAddressService in the event that they’re logged in. We’re going to confirm their login standing using the SessionService.

This half has a setAddressEvent output property. When an take care of is chosen, setAddressEvent emits its id to the daddy or mom half.

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

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

  constructor(
    private session: SessionService,
    private customerAddresses: CustomerAddressService,
    private snackBar: MatSnackBar
  )  

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

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

Proper right here is its template. You possibly can discover its styling here.

<div id="container">
    <p class="mat-headline">Select an current take care of</p>
    <mat-error *ngIf="!addresses.dimension">You don't have any current addresses</mat-error>
    <mat-radio-group *ngIf="addresses.dimension" class="addresses" (change)="setAddress($event)">
        <mat-card class="take care of" *ngFor="let take care of of addresses">
            <mat-radio-button [value]="take care of.take care of?.id" shade="most important">
                <p>take care of.take care of?.firstName take care of.take care of?.lastName,</p>
                <p>take care of.take care of?.line1,</p>
                <p>take care of.take care of?.metropolis,</p>
                <p>take care of.take care of?.zipCode,</p>
                <p>take care of.take care of?.stateCode, take care of.take care of?.countryCode</p>
                <p>take care of.take care of?.cellphone</p>
            </mat-radio-button>
        </mat-card>
    </mat-radio-group>
</div>

Pages

Purchaser Half

An order have to be associated to an e mail take care of. This half is a form that captures the consumer e mail take care of. When the half is initialized, the current purchaser’s e mail take care of is fetched in the event that they’re logged in. We get the consumer from the CustomerService. In the event that they don’t need to change their e mail take care of, this e mail could be the default price.

If the e-mail is modified or a purchaser is simply not logged in, the order is updated with the inputted e mail. We use the OrderService to exchange the order with the model new e mail take care of. If worthwhile, we route the consumer to the billing take care of net web page.

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

  constructor(
    private orders: OrderService,
    private prospects: CustomerService,
    private cart: CartService,
    private router: Router,
    private snackBar: MatSnackBar
  )  

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

  addCustomerEmail() 
    this.orders.updateOrder(
       id: this.cart.orderId, customerEmail: this.e mail.price ,
      [UpdateOrderParams.customerEmail])
      .subscribe(
        () => this.router.navigateByUrl('/billing-address'),
        err => this.snackBar.open('There was a difficulty together with your e mail to the order.', 'Shut',  size: 8000 )
      );
  

Proper right here is the half template and linked here is its styling.

<div id="container">
    <app-title no="1" title="Purchaser" subtitle="Billing information and transport take care of"></app-title>
    <mat-form-field look="outline">
        <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="most important" [disabled]="e mail.invalid" (click on on)="addCustomerEmail()">
        PROCEED TO BILLING ADDRESS
    </button>
</div>

Right here’s a screenshot of the consumer net web page.

Screenshot of customer page

Screenshot of purchaser net web page. (Large preview)

Billing Sort out Half

The billing take care of half lets a purchaser each add a model new billing take care of or select from their current addresses. Prospects who aren’t logged in should enter a model new take care of. People who have logged in get an alternative to decide on between new or current addresses.

The showAddress property signifies whether or not or not current addresses should be confirmed on the half. sameShippingAddressAsBilling signifies whether or not or not the transport take care of should be the equivalent as what the billing take care of is prepared. When a purchaser selects an current take care of, then its id is assigned to selectedCustomerAddressId.

When the half is initialized, we use the SessionService to confirm if the current individual is logged in. In the event that they’re logged in, we’ll present their current addresses in the event that they’ve any.

As talked about earlier, if an individual is logged in, they’ll select an current take care of as their billing take care of. Inside the updateBillingAddress method, in the event that they’re logged in, the take care of they select is cloned and set as a result of the order’s billing take care of. We do this by updating the order using the updateOrder strategy of the OrderService and supplying the take care of Id.

In the event that they don’t appear to be logged in, the individual has to provide an take care of. As quickly as supplied, the take care of is created using the createAddress method. In it, the AddressService takes the enter and makes the model new take care of. After which, the order is updated using the id of the newly created take care of. If there could also be an error or each operation is worthwhile, we current a snackbar.

If the equivalent take care of is chosen as a transport take care of, the individual is routed to the transport methods net web page. Within the occasion that they’d like to provide an alternate transport take care of, they’re directed to the transport take care of net web page.

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

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

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

  updateBillingAddress(take care of: Sort out) 
    if (this.showAddresses && this.selectedCustomerAddressId) 
      this.cloneAddress();
     else if (take care of.firstName && take care of.lastName && take care of.line1 && take care of.metropolis && take care of.zipCode && take care of.stateCode && take care of.countryCode && take care of.cellphone) 
      this.createAddress(take care of);
    
    else 
      this.snackBar.open('Study your take care of. Some fields are missing.', 'Shut');
    
  

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

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

  private createAddress(take care of: Sort out) 
    this.addresses.createAddress(take care of)
      .pipe(
        concatMap(
          take care of => 
            const change = this.updateOrderObservable(
              id: this.cart.orderId,
              billingAddressId: take care of.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()
      );
  

  private 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)
    );
  

  private showErrorSnackBar() 
    this.snackBar.open('There was a difficulty creating your take care of.', 'Shut',  size: 8000 );
  

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

  private showSuccessSnackBar() 
    this.snackBar.open('Billing take care of effectively added. Redirecting...', 'Shut',  size: 3000 );
    if (this.sameShippingAddressAsBilling) 
      this.navigateTo('/shipping-methods');
     else 
      this.navigateTo('/shipping-address');
    
  

Proper right here is the template. This link components to its styling.

<app-title no="2" title="Billing Sort out" subtitle="Sort out to bill charges to"></app-title>
<app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list>
<mat-divider *ngIf="showAddresses"></mat-divider>
<app-address [showTitle]="showAddresses" buttonText="PROCEED TO NEXT STEP" checkboxText="Ship to the equivalent take care of" (isCheckboxChecked)="setSameShippingAddressAsBilling($event)" (createAddress)="updateBillingAddress($event)"></app-address>

That’s what the billing take care of net web page will appear to be.

Screenshot of billing address page

Screenshot of billing take care of net web page. (Large preview)

Transport Sort out Half

The transport take care of half behaves tons identical to the billing take care of half. Nonetheless, there are a couple of variations. For one, the textual content material displayed on the template is totally completely different. The alternative key variations are in how the order is updated using the OrderService as quickly as an take care of is created or chosen. The fields that the order updates are shippingAddressCloneId for chosen addresses and shippingAddress for model spanking new addresses. If an individual chooses to change the billing take care of, to be the equivalent as a result of the transport take care of, the billingAddressSameAsShipping topic is updated.

After a transport take care of is chosen and the order is updated, the individual is routed to the transport methods net web page.

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

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

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

  updateShippingAddress(take care of: Sort out) 
    if (this.showAddresses && this.selectedCustomerAddressId) 
      this.cloneAddress();
     else if (take care of.firstName && take care of.lastName && take care of.line1 && take care of.metropolis && take care of.zipCode && take care of.stateCode && take care of.countryCode && take care of.cellphone) 
      this.createAddress(take care of);
    
    else 
      this.snackBar.open('Study your take care of. Some fields are missing.', 'Shut');
    
  

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

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

  private createAddress(take care of: Sort out) 
    this.addresses.createAddress(take care of)
      .pipe(
        concatMap(
          take care of => 
            const change = this.updateOrderObservable(
              id: this.cart.orderId,
              shippingAddressId: take care of.id
            , [UpdateOrderParams.shippingAddress]);

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

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

  private 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)
    );
  

  private showErrorSnackBar() 
    this.snackBar.open('There was a difficulty creating your take care of.', 'Shut',  size: 8000 );
  

  private showSuccessSnackBar() 
    this.snackBar.open('Transport take care of effectively added. Redirecting...', 'Shut',  size: 3000 );

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

Proper right here is the template and its styling could also be found here.

<app-title no="3" title="Transport Sort out" subtitle="Sort out to ship package deal deal to"></app-title>
<app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list>
<mat-divider *ngIf="showAddresses"></mat-divider>
<app-address [showTitle]="showAddresses" buttonText="PROCEED TO SHIPPING METHODS" checkboxText="Bill to the equivalent take care of" (isCheckboxChecked)="setSameBillingAddressAsShipping($event)" (createAddress)="updateShippingAddress($event)"></app-address>

The transport take care of net web page will appear to be this.

Screenshot of shipping address page

Screenshot of transport take care of net web page. (Large preview)

Transport Methods Half

This half reveals the number of shipments required for an order to be fulfilled, the obtainable transport methods, and their associated costs. The shopper can then select a transport method they like for each cargo.

The shipments property incorporates all the shipments of the order. The shipmentsForm is the form inside which the transport method picks will possible be made.

When the half is initialized, the order is fetched and may comprise every its line devices and shipments. On the equivalent time, we get the provision lead situations for the numerous transport methods. We use the OrderService to get the order and the DeliveryLeadTimeService for the lead situations. As quickly as every models of information are returned, they’re blended into an array of shipments and assigned to the shipments property. Each cargo will comprise its devices, the transport methods obtainable, and the corresponding value.

After the individual has chosen a transport method for each cargo, the chosen transport method is updated for each in setShipmentMethods. If worthwhile, the individual is routed to the funds net web page.

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

  constructor(
    private orders: OrderService,
    private dlts: DeliveryLeadTimeService,
    private cart: CartService,
    private router: Router,
    private fb: FormBuilder,
    private shipments: ShipmentService,
    private 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(
              method => 
                method.deliveryLeadTime = this.findMethodLeadTime(lt, method);
                return method;
              );
          

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

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

    combineLatest(Object.keys(shipmentsFormValue).map(
      key => this.shipments.updateShipment(key, shipmentsFormValue[key])
    )).subscribe(
      () => 
        this.snackBar.open('Your shipments have been updated with a transport method.', 'Shut',  size: 3000 );
        setTimeout(() => this.router.navigateByUrl('/payment'), 4000);
      ,
      err => this.snackBar.open('There was a difficulty together with transport methods to your shipments.', 'Shut',  size: 5000 )
    );
  


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

  private findLocationLeadTime(situations: DeliveryLeadTime[], cargo: Cargo): DeliveryLeadTime[] 
    return situations.filter((dlTime) => dlTime?.stockLocation?.id == cargo?.stockLocation?.id);
  

  private findMethodLeadTime(situations: DeliveryLeadTime[], method: ShippingMethod): DeliveryLeadTime 
    return situations.filter((dlTime) => dlTime?.shippingMethod?.id == method?.id)[0];
  

Proper right here is the template and also you’ll discover the styling at this link.

<form id="container" [formGroup]="shipmentsForm">
    <app-title no="4" title="Transport Methods" subtitle=" ship your packages"></app-title>
    <div class="shipment-container" *ngFor="let cargo of shipments; let j = index; let isLast = ultimate">
        <h1>Cargo j+1 of shipments?.dimension</h1>
        <div class="row" *ngFor="let merchandise of cargo.lineItems">
            <img class="image-xs" [src]="merchandise.imageUrl" alt="product image">
            <div id="shipment-details">
                <h4 id="item-name">merchandise.title</h4>
                <p>merchandise.skuCode</p>
            </div>
            <div id="quantity-section">
                <p id="quantity-label">Quantity: </p>merchandise.quantity
            </div>
        </div>
        <mat-radio-group [formControlName]="cargo?.id || j">
            <mat-radio-button *ngFor="let strategy of cargo.availableShippingMethods" [value]="method.id">
                <div class="radio-button">
                    <p>method.title</p>
                    <div>
                        <p class="radio-label">Worth:</p>
                        <p> method.formattedPriceAmount</p>
                    </div>
                    <div>
                        <p class="radio-label">Timeline:</p>
                        <p> Obtainable in method.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="most important" [disabled]="shipmentsForm.invalid" (click on on)="setShipmentMethods()">PROCEED TO PAYMENT</button>
</form>

This could be a screenshot of the transport methods net web page.

Screenshot of shipping methods page

Screenshot of transport methods net web page. (Large preview)

Funds Half

On this half, the individual clicks the payment button within the occasion that they need to proceed to pay for his or her order with Paypal. The approvalUrl is the Paypal hyperlink that the individual is directed to after they click on on the button.

All through initialization, we get the order with the payment provide included using the OrderService. If a payment provide is prepared, we get its id and retrieve the corresponding Paypal payment from the PaypalPaymentService. The Paypal payment will comprise the approval url. If no payment provide has been set, we change the order with Paypal as the favored payment method. We then proceed to create a model new Paypal payment for the order using the PaypalPaymentService. From proper right here, we’ll get the approval url from the newly created order.

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

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

  constructor(
    private orders: OrderService,
    private cart: CartService,
    private router: Router,
    private 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;
  

Proper right here is its template.

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

Proper right here’s what the funds net web page will appear to be.

Screenshot of payment page

Screenshot of payment net web page. (Large preview)

Cancel Value Half

Paypal requires a cancel payment net web page. This half serves this goal. That’s its template.

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

Proper right here’s a screenshot of the net web page.

Screenshot of payment cancellation page

Screenshot of payment cancellation net web page. (Large preview)

Place Order Half

That’s the ultimate step throughout the checkout course of. Proper right here the individual confirms that they definitely want to put the order and begin its processing. When the individual approves the Paypal payment, that’s the net web page they’re redirected to. Paypal offers a payer id query parameter to the url. That’s the individual’s Paypal Id.

When the half is initialized, we get the payerId query parameter from the url. The order is then retrieved using the OrderService with the payment provide included. The id of the included payment provide is used to exchange the Paypal payment with the payer id, using the PaypalPayment service. If any of these fail, the individual is redirected to the error net web page. We use the disableButton property to cease the individual from inserting the order until the payer Id is prepared.

After they click on on the place-order button, the order is updated with a positioned standing. Afterwhich the cart is cleared, a worthwhile snack bar is displayed, and the individual is redirected to the home net web page.

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

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

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

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

                  return iif(
                    () => paymentSourceId ? paymentSourceId.dimension > 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 effectively positioned.', 'Shut',  size: 3000 );
          this.cart.clearCart();
          setTimeout(() => this.router.navigateByUrl("https://smashingmagazine.com/"), 4000);
        ,
        () => 
          this.snackBar.open('There was a difficulty inserting your order.', 'Shut',  size: 8000 );
          this.disableButton = false;
        
      );
  

Proper 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>

Right here’s a screenshot of the net web page.

Screenshot of order placement page

Screenshot of order placement net web page. (Large preview)

App Module

All requests made to Commerce Layer, aside from for authentication, must 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 provide an initialization function by which the token is retrieved. Furthermore, we’ll use the HTTP_INTERCEPTORS token to provide the OptionsInterceptor we created earlier. As quickly as all the modules are added the app module file should look one factor 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 Half

We’ll modify the app half template and its styling which you’ll discover here.

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

Conclusion

On this text, we’ve lined the way you would possibly create an e-commerce Angular 11 app with Commerce Layer and Paypal. We’ve moreover touched on straightforward strategies to building the app and the way in which you would possibly interface with an e-commerce API.

Although this app permits a purchaser to make a complete order, it isn’t by any means accomplished. There could also be so much you would possibly add to reinforce it. For one, it’s potential you’ll choose to permit merchandise quantity changes throughout the cart, hyperlink cart devices to their product pages, optimize the take care of components, add further guards for checkout pages identical to the place-order net web page, and so forth. That’s merely the beginning line.

While you’d like to understand further regarding the course of of making an order from start to finish, you would possibly check out the Commerce Layer guides and API. You’ll be capable of view the code for this problem at this repository.

(vf, yk, il)

Source link

Translate »