How To Construct An E-Commerce Web site With Angular 11, Commerce Layer And Paypal

No Comments

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

Websites reminiscent of Etsy, Shopify and Amazon make it straightforward to arrange a retailer fairly rapidly with out having to fret about growing a website. Nevertheless, there could also be situations the place store house owners might need a personalised expertise or possibly save on the price of proudly owning a retailer on a few of these platforms.

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

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

View supply code on GitHub →

Stipulations

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

npm set up -g @angular/cli

You’ll additionally want a Commerce Layer developer account. Utilizing the developer account, you will want to create a check group and seed it with check information. Seeding makes it simpler to develop the app first with out worrying about what information you’ll have to make use of. You may create an account at this hyperlink and a company right here.

Lastly, you will want a Paypal Sandbox account. Having any such account will permit us to check transactions between companies and customers with out risking precise cash. You may create one right here. A sandbox account has a check enterprise and check private account already created for it.

Commerce Layer And Paypal Config

To make Paypal Sandbox funds potential on Commerce Layer, you’ll must arrange API keys. Head on over to the accounts overview of your Paypal developer account. Choose a enterprise account and underneath the API credentials tab of the account particulars, you can see the Default Software underneath REST Apps.

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

After creating the gateway, you will want to create a Paypal fee technique for every market you might be concentrating on to make Paypal accessible as an choice. You’ll do that underneath Settings > Funds > Fee Strategies > New Fee Methodology.

A Observe About Routes Used

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

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

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

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

App Construction

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

the core module,
the info module,
the shared module,
the characteristic modules.

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

the auth module,
the product module,
the cart module,
the checkout module.

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

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

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

Producing The App And Including Dependencies

We’ll start by producing the app. Our group will likely be referred to as The LIme Model and may have check information already seeded by Commerce Layer.

ng new lime-app

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

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

Belongings

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

Kinds

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

Setting

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

Shared Module

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

ng g m shared

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

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

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

@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 { }

Elements

Merchandise Amount Element

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

ng g c shared/elements/item-quantity

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

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

This is the part file.

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

values: quantity[] = [];

constructor() { }

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

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

That is its template.

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

Right here is its styling.

button {
margin: 3px;
}

Title Element

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

ng g c shared/elements/title

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

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

Under is its template. You will discover its styling linked right here.

<div id=”header”>
<h1 *ngIf=”no” class=”mat-display-1″ id=”no”>{{no}}</h1>
<div [ngClass]=”{ ‘centered-section’: centerText}”>
<h1 class=”mat-display-2″>{{title}}</h1>
<p id=”subheading”>{{subtitle}}</p>
</div>
</div>

Easy Web page Element

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

To make it:

ng g c shared/elements/simple-page

This is its part file.

@Element({
selector: ‘app-simple-page’,
templateUrl: ‘./simple-page.part.html’,
styleUrls: [‘./simple-page.component.css’]
})
export class SimplePageComponent {
@Enter() title: string = ”;
@Enter() subtitle?: string;
@Enter() quantity?: string;
@Enter() icon?: string;
@Enter() buttonText: string = ”;
@Enter() centerText?: boolean = false;
@Enter() buttonDisabled?: boolean = false;
@Enter() route?: string | undefined;
@Output() buttonEvent = new EventEmitter();

constructor(personal router: Router) { }

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

And its template comprises:

<div id=”container”>
<app-title no=”{{quantity}}” title=”{{title}}” subtitle=”{{subtitle}}” [centerText]=”centerText”></app-title>
<div *ngIf=”icon” id=”icon-container”>
<mat-icon colour=”major” class=”icon”>{{icon}}</mat-icon>
</div>
<button mat-flat-button colour=”major” (click on)=”buttonClicked()” [disabled]=”buttonDisabled”>
{{buttonText}}
</button>
</div>

It’s styling will be discovered right here.

Pipes

Phrase Wrap Pipe

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

To create it run:

ng g pipe shared/pipes/word-wrap

It would include:

import { Pipe, PipeTransform } from ‘@angular/core’;

@Pipe({
identify: ‘wordWrap’
})
export class WordWrapPipe implements PipeTransform {
remodel(worth: string, size: quantity): string {
return `${worth.substring(0, size)}…`;
}
}

Providers

HTTP Error Handler Service

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

Generate it by working:

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

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

@Injectable({
providedIn: ‘root’
})
export class HttpErrorHandler {

constructor() { }

handleError(err: HttpErrorResponse): Observable {
let displayMessage = ”;

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

return throwError(displayMessage);
}
}

Native Storage Service

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

To generate the native storage service run:

ng g s shared/companies/local-storage

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

import { Injectable } from ‘@angular/core’;

@Injectable({
providedIn: ‘root’
})
export class LocalStorageService {

constructor() { }

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

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

getItem(key: string): string | null {
return localStorage.getItem(key);
}

clear() {
localStorage.clear();
}
}

Information Module

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

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

To generate the module run:

ng g m information

Fashions

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

for mannequin in
deal with cart nation customer-address
buyer delivery-lead-time line-item order
payment-method payment-source paypal-payment
worth cargo shipping-method sku stock-location;
do ng g interface “information/fashions/${mannequin}”; executed

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

Interface
Description

Handle
Represents a basic deal with.

Cart
Consumer aspect model of an order monitoring the variety of merchandise a buyer intends to buy.

Nation
Alpha-2 nation code.

Buyer Handle
An deal with related to a buyer.

Buyer
A registered person.

Supply Lead Time
Represents the period of time it’s going to take to supply a cargo.

Line Merchandise
An itemized product added to the cart.

Order
A purchasing cart or assortment of line objects.

Fee Methodology
A fee sort made accessible to an order.

Fee Supply
A fee related to an order.

Paypal Fee
A fee made by Paypal

Value
Value related to an SKU.

Cargo
Assortment of things shipped collectively.

Transport Methodology
Methodology by which a package deal is shipped.

SKU
A singular stock-keeping unit.

Inventory Location
Location that comprises SKU stock.

Providers

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

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

Handle Service

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

The route used right here is /api/addresses. When you’re going to make use of the Commerce Layer API immediately, make sure that to construction the info as demonstrated in this instance.

@Injectable({
providedIn: ‘root’
})
export class AddressService {
personal url: string = ${setting.apiUrl}/api/addresses;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

createAddress(deal with: Handle): Observable<Handle> {
return this.http.put up<Handle>(this.url, deal with)
.pipe(catchError(this.eh.handleError));
}

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

Cart Service

The cart is accountable for sustaining the amount of things added and the order Id. Making API calls to get the variety of objects in an order everytime a brand new line merchandise is created will be costly. As an alternative, we might simply use native storage to take care of the depend on the shopper. This eliminates the necessity to make pointless order fetches each time an merchandise is added to the cart.

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

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

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

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

@Injectable({
providedIn: ‘root’
})
export class CartService {
personal cart = new BehaviorSubject({
orderId: this.orderId,
itemCount: this.itemCount
});

cartValue$ = this.cart.asObservable();

constructor(personal storage: LocalStorageService) { }

get orderId(): string {
const id = this.storage.getItem(‘order-id’);
return id ? id : ”;
}

set orderId(id: string) {
this.storage.addItem(‘order-id’, id);
this.cart.subsequent({ orderId: id, itemCount: this.itemCount });
}

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

return itemCount ? parseInt(itemCount) : 0;
}

set itemCount(quantity: quantity) {
this.storage.addItem(‘item-count’, quantity.toString());
this.cart.subsequent({ orderId: this.orderId, itemCount: quantity });
}

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

decrementItemCount(quantity: quantity) {
this.itemCount = this.itemCount – quantity;
}

clearCart() {
this.storage.deleteItem(‘item-count’);
this.cart.subsequent({ orderId: ”, itemCount: 0 });
}
}

Nation Service

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

@Injectable({
providedIn: ‘root’
})
export class CountryService {

constructor(personal http: HttpClient) { }

getCountries(): Observable<Nation[]> {
return this.http.get<Nation[]>(‘./../../../property/json/country-codes.json’);
}
}

Buyer Handle Service

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

When making a buyer deal with, you should definitely construction the put up physique based on this instance.

@Injectable({
providedIn: ‘root’
})
export class CustomerAddressService {
personal url: string = ${setting.apiUrl}/api/customer_addresses;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

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

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

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

Buyer Service

Prospects are created and their info retrieved utilizing this service. When a person indicators up, they develop into a buyer and are created utilizing the createCustomerMethod. getCustomer returns the client related to a particular Id. getCurrentCustomer returns the client at the moment logged in.

When making a buyer, construction the info like this. You may add their first and final names to the metadata, as proven in its attributes.

The route /api/prospects/present shouldn’t be accessible on Commerce Layer. So that you’ll want to determine how one can get the at the moment logged in buyer.

@Injectable({
providedIn: ‘root’
})
export class CustomerService {
personal url: string = ${setting.apiUrl}/api/prospects;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

createCustomer(electronic mail: string, password: string, firstName: string, lastName: string): Observable<Buyer> {
return this.http.put up<Buyer>(this.url, {
electronic mail: electronic mail,
password: password,
firstName: firstName,
lastName: lastName
})
.pipe(catchError(this.eh.handleError));
}

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

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

Supply Lead Time Service

This service returns details about transport timelines from numerous inventory areas.

@Injectable({
providedIn: ‘root’
})
export class DeliveryLeadTimeService {
personal url: string = ${setting.apiUrl}/api/delivery_lead_times;

constructor(personal http: HttpClient, personal 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’ll be able to create an merchandise the second it’s added to the cart. An merchandise’s info will also be fetched. The merchandise can also be up to date when its amount adjustments or deleted when faraway from the cart.

When creating objects or updating them, construction the request physique as proven on this instance.

@Injectable({
providedIn: ‘root’
})
export class LineItemService {
personal url: string = ${setting.apiUrl}/api/line_items;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

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

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

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

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

Order Service

Much like the road merchandise service, the order service permits you to create, replace, delete, or get an order. Moreover, you could select to get the shipments related to an order individually utilizing the getOrderShipments technique. This service is used closely all through the checkout course of.

There are completely different sorts of details about an order which can be required all through checkout. Since it could be costly to fetch a complete order and its relations, we specify what we wish to get from an order utilizing GetOrderParams. The equal of this on the CL API is the embody question parameter the place you checklist the order relationships to be included. You may examine what fields must be included for the cart abstract right here and for the assorted checkout levels right here.

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

@Injectable({
providedIn: ‘root’
})
export class OrderService {
personal url: string = ${setting.apiUrl}/api/orders;

constructor(
personal http: HttpClient,
personal eh: HttpErrorHandler) { }

createOrder(): Observable<Order> {
return this.http.put up<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: { ‘area’: 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 Fee Service

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

@Injectable({
providedIn: ‘root’
})
export class PaypalPaymentService {
personal url: string = ${setting.apiUrl}/api/paypal_payments;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

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

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

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

Cargo Service

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

@Injectable({
providedIn: ‘root’
})
export class ShipmentService {
personal url: string = ${setting.apiUrl}/api/shipments;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

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

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

SKU Service

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

@Injectable({
providedIn: ‘root’
})
export class SkuService {
personal url: string = ${setting.apiUrl}/api/skus;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

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

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

Core Module

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

The core module tree will appear like this.

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

To generate the module and its contents run:

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

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

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

Providers

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

Authentication Service

The AuthenticationService permits you to purchase shopper and buyer tokens. These tokens are used to entry the remainder of the API’s routes. Buyer tokens are returned when a person exchanges an electronic mail and password for it and have a wider vary of permissions. Consumer tokens are issued without having credentials and have narrower permissions.

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

@Injectable({
providedIn: ‘root’
})
export class AuthenticationService {
personal url: string = ${setting.apiUrl}/oauth/token;

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

getClientSession(): Observable<object> {
return this.http.put up<object>(
this.url,
{ grantType: ‘client_credentials’ })
.pipe(catchError(this.eh.handleError));
}

login(electronic mail: string, password: string): Observable<object> {
return this.http.put up<object>(
this.url,
{ username: electronic mail, password: password, grantType: ‘password’ })
.pipe(catchError(this.eh.handleError));
}
}

Session Service

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

@Injectable({
providedIn: ‘root’
})
export class SessionService {
personal url: string = ${setting.apiUrl}/session;
personal isLoggedIn = new BehaviorSubject(false);

loggedInStatus = this.isLoggedIn.asObservable();

constructor(personal http: HttpClient, personal eh: HttpErrorHandler) { }

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

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

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

Header Service

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

@Injectable({
providedIn: ‘root’
})
export class HeaderService {
personal headerButtonsVisibility = new BehaviorSubject(true);

showHeaderButtons = this.headerButtonsVisibility.asObservable();

constructor() { }

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

Elements

Error Element

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

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

That is what the part will appear like.

Not Discovered Element

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

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

Header Element

The HeaderComponent is mainly the header displayed on the high of a web page. It would include the app title, the cart, login, and logout buttons.

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

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

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

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

constructor(
personal session: SessionService,
personal snackBar: MatSnackBar,
personal cart: CartService,
personal header: HeaderService,
personal auth: AuthenticationService
) { }

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

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

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

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

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

Under is the header template and linked right here is its styling.

<div id=”header-container”>
<div id=”left-half” routerLink=”/”>
<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 colour=”major” aria-label=”purchasing cart”>
<mat-icon [matBadge]=”cartAmount” matBadgeColor=”accent” aria-label=”purchasing cart” routerLink=”/cart”>shopping_cart</mat-icon>
</button>
<button mat-icon-button colour=”major” aria-label=”login”
ngIf=”!isLoggedIn”>
<mat-icon aria-label=”login” matTooltip=”login” routerLink=”/login”>login</mat-icon>
</button>
<button mat-icon-button colour=”major” aria-label=”logout” *ngIf=”isLoggedIn” (click on)=”logout()”>
<mat-icon aria-label=”logout” matTooltip=”logout”>logout</mat-icon>
</button>
</div>
</div>
</div>

Guards

Empty Cart Guard

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

@Injectable({
providedIn: ‘root’
})
export class EmptyCartGuard implements CanActivate {
constructor(personal cart: CartService, personal router: Router) { }

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (this.cart.orderId) {
if (this.cart.itemCount > 0) {
return true;
}
}

return this.router.parseUrl(‘/empty’);
}
}

Interceptors

Choices Interceptor

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

@Injectable()
export class OptionsInterceptor implements HttpInterceptor {

constructor() { }

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

return subsequent.deal with(request);
}
}

Characteristic Modules

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

Merchandise Module

The merchandise module comprises pages that show merchandise on sale. These embody the product web page and the product checklist web page. It’s structured as proven under.

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

To generate it and its elements:

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

That is the module file:

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

Product Checklist Element

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

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

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

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

@UntilDestroy({ checkProperties: true })
@Element({
selector: ‘app-product-list’,
templateUrl: ‘./product-list.part.html’,
styleUrls: [‘./product-list.component.css’]
})
export class ProductListComponent implements OnInit {
cols = 4;
size = 0;
pageIndex = 0;
pageSize = 20;
pageSizeOptions: quantity[] = [5, 10, 20];

pageEvent!: PageEvent | void;

merchandise: Sku[] = [];

constructor(
personal breakpointObserver: BreakpointObserver,
personal skus: SkuService,
personal router: Router,
personal header: HeaderService) { }

ngOnInit() {
this.getProducts(1, 20);
this.header.setHeaderButtonsVisibility(true);

this.breakpointObserver.observe([
Breakpoints.Handset,
Breakpoints.Tablet,
Breakpoints.Web
]).subscribe(consequence => {
if (consequence.matches) {
if (consequence.breakpoints[‘(max-width: 599.98px) and (orientation: portrait)’] || consequence.breakpoints[‘(max-width: 599.98px) and (orientation: landscape)’]) {
this.cols = 1;
}
else if (consequence.breakpoints[‘(min-width: 1280px) and (orientation: portrait)’] || consequence.breakpoints[‘(min-width: 1280px) and (orientation: landscape)’]) {
this.cols = 4;
} else {
this.cols = 3;
}
}
});
}

personal getProducts(web page: quantity, pageSize: quantity) {
this.skus.getSkus(web page, pageSize)
.subscribe(
skus => {
this.merchandise = skus;
this.size = skus[0].__collectionMeta.recordCount;
},
err => this.router.navigateByUrl(‘/error’)
);
}

getNextPage(occasion: PageEvent) {
this.getProducts(occasion.pageIndex + 1, occasion.pageSize);
}

trackSkus(index: quantity, merchandise: Sku) {
return ${merchandise.id}-${index};
}
}

The template is proven under and its styling will be discovered right 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=”{{product.imageUrl}}” alt=”product photograph”>
<mat-card-content>
<mat-card-title matTooltip=”{{product.identify}}”>{wordWrap:35}</mat-card-title>
<mat-card-subtitle>{ forex:’EUR’}</mat-card-subtitle>
</mat-card-content>
<mat-card-actions>
<button mat-flat-button colour=”major” [routerLink]=”[‘/product’, product.id]”>
View
</button>
</mat-card-actions>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
<mat-paginator [length]=”size” [pageIndex]=”pageIndex” [pageSize]=”pageSize” [pageSizeOptions]=”pageSizeOptions” (web page)=”pageEvent = getNextPage($occasion)”>
</mat-paginator>

The web page will appear like this.

Product Element

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

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

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

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

constructor(
personal route: ActivatedRoute,
personal skus: SkuService,
personal location: Location,
personal router: Router,
personal header: HeaderService,
personal orders: OrderService,
personal lineItems: LineItemService,
personal cart: CartService,
personal snackBar: MatSnackBar
) { }

ngOnInit() {
this.route.paramMap
.pipe(
mergeMap(params => {
const id = params.get(‘id’)
this.id = id ? id : ”;

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

this.header.setHeaderButtonsVisibility(true);
}

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

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

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

goBack() {
this.location.again();
}

personal showSuccessSnackBar() {
this.snackBar.open(‘Merchandise efficiently added to cart.’, ‘Shut’, { length: 8000 });
}

personal showErrorSnackBar() {
this.snackBar.open(‘Failed so as to add your merchandise to the cart.’, ‘Shut’, { length: 8000 });
}
}

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

<div id=”container”>
<mat-card *ngIf=”product” class=”product-card”>
<img mat-card-image src=”{{product.imageUrl}}” alt=”Picture of a product”>
<mat-card-content>
<mat-card-title>{{product.identify}}</mat-card-title>
<mat-card-subtitle>{ forex:’EUR’}</mat-card-subtitle>
<p>
{{product.description}}
</p>
</mat-card-content>
<mat-card-actions>
<app-item-quantity [quantity]=”amount” [maxValue]=”10″ (setQuantityEvent)=”setQuantity($occasion)”></app-item-quantity>
<button mat-raised-button colour=”accent” (click on)=”addItemToCart()”>
<mat-icon>add_shopping_cart</mat-icon>
Add to cart
</button>
<button mat-raised-button colour=”major” (click on)=”goBack()”>
<mat-icon>storefront</mat-icon>
Proceed purchasing
</button>
</mat-card-actions>
</mat-card>
</div>

The web page will appear like this.

Auth Module

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

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

To generate it and its elements:

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

That is its module file.

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

Signup Element

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

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

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

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

@ViewChild(FormGroupDirective) sufDirective: FormGroupDirective | undefined;

constructor(
personal buyer: CustomerService,
personal fb: FormBuilder,
personal snackBar: MatSnackBar,
personal router: Router,
personal header: HeaderService
) { }

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

matchPasswords(signupGroup: AbstractControl): ValidationErrors | null {
const password = signupGroup.get(‘password’)?.worth;
const confirmedPassword = signupGroup.get(‘confirmedPassword’)?.worth;

return password == confirmedPassword ? null : { differentPasswords: true };
}

get password() { return this.signupForm.get(‘password’); }

get confirmedPassword() { return this.signupForm.get(‘confirmedPassword’); }

signup() {
const buyer = this.signupForm.worth;

this.buyer.createCustomer(
buyer.electronic mail,
buyer.password,
buyer.firstName,
buyer.lastName
).subscribe(
() => {
this.signupForm.reset();
this.sufDirective?.resetForm();

this.snackBar.open(‘Account efficiently created. You’ll be redirected in 5 seconds.’, ‘Shut’, { length: 5000 });

setTimeout(() => this.router.navigateByUrl(‘/’), 6000);
},
err => this.snackBar.open(‘There was an issue creating your account.’, ‘Shut’, { length: 5000 })
);
}
}

Under is the template for the SignupComponent.

<kind id=”container” [formGroup]=”signupForm” (ngSubmit)=”signup()”>
<h1 class=”mat-display-3″>Create Account</h1>
<mat-form-field look=”define”>
<mat-label>First Title</mat-label>
<enter matInput formControlName=”firstName”>
<mat-icon matPrefix>portrait</mat-icon>
</mat-form-field>
<mat-form-field look=”define”>
<mat-label>Final Title</mat-label>
<enter matInput formControlName=”lastName”>
<mat-icon matPrefix>portrait</mat-icon>
</mat-form-field>
<mat-form-field look=”define”>
<mat-label>E mail</mat-label>
<enter matInput formControlName=”electronic mail” sort=”electronic mail”>
<mat-icon matPrefix>alternate_email</mat-icon>
</mat-form-field>
<mat-form-field look=”define”>
<mat-label>Password</mat-label>
<enter matInput formControlName=”password” sort=”password”>
<mat-icon matPrefix>vpn_key</mat-icon>
</mat-form-field>
<mat-form-field look=”define”>
<mat-label>Affirm Password</mat-label>
<enter matInput formControlName=”confirmedPassword” sort=”password”>
<mat-icon matPrefix>vpn_key</mat-icon>
</mat-form-field>
<div ngIf=”confirmedPassword?.invalid && (confirmedPassword?.soiled || confirmedPassword?.touched)”>
<mat-error
ngIf=”signupForm.hasError(‘differentPasswords’)”>
Your passwords don’t match.
</mat-error>
</div>
<div ngIf=”password?.invalid && (password?.soiled || password?.touched)”>
<mat-error
ngIf=”password?.hasError(‘minlength’)”>
Your password needs to be not less than 8 characters.
</mat-error>
</div>
<button mat-flat-button colour=”major” [disabled]=”!signupForm.legitimate”>Signal Up</button>
</kind>

The part will end up as follows.

Login Element

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

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

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

@UntilDestroy({ checkProperties: true })
@Element({
selector: ‘app-login’,
templateUrl: ‘./login.part.html’,
styleUrls: [‘./login.component.css’]
})
export class LoginComponent implements OnInit {
loginForm = this.fb.group({
electronic mail: [”, Validators.required],
password: [”, Validators.required]
});

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

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

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

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

this.loginForm.patchValue({ password: ” });
}
);
}
}

Under is the LoginComponent template.

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

Here’s a screenshot of the web page.

Cart Module

The cart module comprises all pages associated to the cart. These embody the order abstract web page, a coupon and present card code web page, and an empty cart web page. It is structured as follows.

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

To generate it, run:

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

That is the module file.

@NgModule({
declarations: [SummaryComponent, CodesComponent, EmptyComponent],
imports: [
RouterModule.forChild([
{
path: ”, canActivate: [EmptyCartGuard], kids: [
{ path: ‘cart’, component: SummaryComponent },
{ path: ‘codes’, component: CodesComponent }
]
},
{ path: ’empty’, part: EmptyComponent }
]),
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatMenuModule,
ReactiveFormsModule,
SharedModule
]
})
export class CartModule { }
Codes Element

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

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

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

@UntilDestroy({ checkProperties: true })
@Element({
selector: ‘app-codes’,
templateUrl: ‘./codes.part.html’,
styleUrls: [‘./codes.component.css’]
})
export class CodesComponent {
couponCode = new FormControl(”);
giftCardCode = new FormControl(”);

@ViewChild(FormControlDirective) codesDirective: FormControlDirective | undefined;

constructor(
personal cart: CartService,
personal order: OrderService,
personal snackBar: MatSnackBar
) { }

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

addCoupon() {
this.updateOrder({ id: this.cart.orderId, couponCode: this.couponCode.worth }, [UpdateOrderParams.couponCode], ‘coupon’);
}

addGiftCard() {
this.updateOrder({ id: this.cart.orderId, giftCardCode: this.giftCardCode.worth }, [UpdateOrderParams.giftCardCode], ‘present card’);
}

}

The template is proven under and its styling will be discovered at this hyperlink.

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

Here’s a screenshot of the web page.

Empty Element

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

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

<app-simple-page title=”Your cart is empty” subtitle=”There’s at the moment nothing in your cart. Head to the house web page so as to add objects.” buttonText=”GO TO HOME PAGE” icon=”shopping_basket” [centerText]=”true” route=”/”>
</app-simple-page>

Here’s a screenshot of the web page.

Abstract Element

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

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

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

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

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

constructor(
personal orders: OrderService,
personal lineItems: LineItemService,
personal cart: CartService,
personal snackBar: MatSnackBar,
personal router: Router
) { }

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

personal processOrder(order: Order) {
this.order = order;

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

personal showOrderError(msg: string) {
this.snackBar.open(There was an issue ${msg}., ‘Shut’, { length: 8000 });
}

checkout() {
this.router.navigateByUrl(‘/buyer’);
}

deleteLineItem(id: string) {
this.lineItems.deleteLineItem(id)
.pipe(
mergeMap(() => this.orders.getOrder(this.cart.orderId, GetOrderParams.cart))
).subscribe(
order => {
this.processOrder(order);
this.cart.itemCount = order.skusCount || this.cart.itemCount;
this.snackBar.open(Merchandise efficiently faraway from cart., ‘Shut’, { length: 8000 })
},
err => this.showOrderError(‘deleting your order’)
);
}
}

Under is the template and its styling is linked right here.

<div class=”container” ngIf=”order”>
<h3 id=”order-id”>Order #{{order.quantity}} ({{order.skusCount}} objects)</h3>
<div class=”line-item”
ngFor=”let merchandise of order.lineItems”>
<div id=”product-details”>
<img ngIf=”merchandise.imageUrl” class=”image-xs” src=”{{merchandise.imageUrl}}” alt=”product photograph”>
<div
ngIf=”!merchandise.imageUrl” class=”image-xs no-image”></div>
<div id=”line-details”>
<div>{{merchandise.identify}}</div>
<div> {{merchandise.formattedUnitAmount }} </div>
</div>
</div>
<div id=”product-config”>
<app-item-quantity [quantity]=”merchandise.amount || 0″ [disabled]=”true”></app-item-quantity>
<div class=”itemTotal”> {{merchandise.formattedTotalAmount }} </div>
<button mat-icon-button colour=”warn” (click on)=”deleteLineItem(merchandise.id || ”)”>
<mat-icon>clear</mat-icon>
</button>
</div>
</div>
<mat-divider></mat-divider>
<div class=”costSummary”>
<div class=”costItem” *ngFor=”let merchandise of abstract” [id]=”merchandise.id”>
<h3 class=”costLabel”>{{merchandise.identify}}</h3>
<p> {{merchandise.quantity }} </p>
</div>
</div>
<mat-divider></mat-divider>
<div class=”costSummary”>
<div class=”costItem” id=”whole”>
<h2 id=”totalLabel”>Whole</h2>
<h2> {{order.formattedTotalAmountWithTaxes}} </h2>
</div>
</div>
<div id=”checkout-button”>
<button colour=”accent” mat-flat-button routerLink=”/codes”>
<mat-icon>redeem</mat-icon>
ADD GIFT CARD/COUPON
</button>
<button colour=”major” mat-flat-button (click on)=”checkout()”>
<mat-icon>point_of_sale</mat-icon>
CHECKOUT
</button>
</div>
</div>

Here’s a screenshot of the web page.

Checkout Module

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

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

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

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

That is the module file.

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

Nation Choose Element

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

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

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

@UntilDestroy({ checkProperties: true })
@Element({
selector: ‘app-country-select’,
templateUrl: ‘./country-select.part.html’,
styleUrls: [‘./country-select.component.css’]
})
export class CountrySelectComponent implements OnInit {
nation: string = ‘Nation’;
nations: Nation[] = [];
@Output() setCountryEvent = new EventEmitter<string>();

constructor(personal nations: CountryService) { }

ngOnInit() {
this.nations.getCountries()
.subscribe(
nations => {
this.nations = nations;
}
);
}

setCountry(worth: Nation) ”;
this.setCountryEvent.emit(worth.code);
}

Under is its template and linked right here is its styling.

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

Handle Element

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

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

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

@Element({
selector: ‘app-address’,
templateUrl: ‘./deal with.part.html’,
styleUrls: [‘./address.component.css’]
})
export class AddressComponent {
@Enter() buttonText: string = ”;
@Enter() showTitle?: boolean = false;

@Output() createAddress = new EventEmitter<Handle>();

@Enter() checkboxText: string = ”;
@Output() isCheckboxChecked = new EventEmitter<boolean>();

countryCode: string = ”;

addressForm = this.fb.group({
firstName: [”],
lastName: [”],
line1: [”],
metropolis: [”],
zipCode: [”],
stateCode: [”],
cellphone: [”]
});

@ViewChild(FormGroupDirective) afDirective: FormGroupDirective | undefined;

constructor(personal fb: FormBuilder) { }

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

addAddress() {
this.createAddress.emit();
}

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

That is its template and its styling is linked right here.

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

Handle Checklist Element

When a buyer logs in, they will entry their current addresses. As an alternative of getting them re-enter an deal with, they will decide from an deal with checklist. That is the aim of this part. On initialization, all the client’s addresses are fetched utilizing the CustomerAddressService if they’re logged in. We’ll examine their login standing utilizing the SessionService.

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

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

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

constructor(
personal session: SessionService,
personal customerAddresses: CustomerAddressService,
personal snackBar: MatSnackBar
) { }

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

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

Right here is its template. You will discover its styling right here.

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

Buyer Element

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

If the e-mail is modified or a buyer shouldn’t be logged in, the order is up to date with the inputted electronic mail. We use the OrderService to replace the order with the brand new electronic mail deal with. If profitable, we route the client to the billing deal with web page.

@UntilDestroy({ checkProperties: true })
@Element({
selector: ‘app-customer’,
templateUrl: ‘./buyer.part.html’,
styleUrls: [‘./customer.component.css’]
})
export class CustomerComponent implements OnInit {
electronic mail = new FormControl(”, [Validators.required, Validators.email]);

constructor(
personal orders: OrderService,
personal prospects: CustomerService,
personal cart: CartService,
personal router: Router,
personal snackBar: MatSnackBar
) { }

ngOnInit() {
this.prospects.getCurrentCustomer()
.subscribe(
buyer => this.electronic mail.setValue(buyer.electronic mail)
);
}

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

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

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

Here’s a screenshot of the client web page.

Billing Handle Element

The billing deal with part lets a buyer both add a brand new billing deal with or decide from their current addresses. Customers who will not be logged in need to enter a brand new deal with. Those that have logged in get an choice to select between new or current addresses.

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

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

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

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

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

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

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

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

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

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

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

personal createAddress(deal with: Handle) {
this.addresses.createAddress(deal with)
.pipe(
concatMap(
deal with => {
const replace = this.updateOrderObservable({
id: this.cart.orderId,
billingAddressId: deal with.id
}, [UpdateOrderParams.billingAddress]);

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

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

personal 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)
);
}

personal showErrorSnackBar() {
this.snackBar.open(‘There was an issue creating your deal with.’, ‘Shut’, { length: 8000 });
}

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

personal showSuccessSnackBar() {
this.snackBar.open(‘Billing deal with efficiently added. Redirecting…’, ‘Shut’, { length: 3000 });
if (this.sameShippingAddressAsBilling) {
this.navigateTo(‘/shipping-methods’);
} else {
this.navigateTo(‘/shipping-address’);
}
}
}

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

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

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

Transport Handle Element

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

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

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

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

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

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

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

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

personal createAddress(deal with: Handle) {
this.addresses.createAddress(deal with)
.pipe(
concatMap(
deal with => {
const replace = this.updateOrderObservable({
id: this.cart.orderId,
shippingAddressId: deal with.id
}, [UpdateOrderParams.shippingAddress]);

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

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

personal 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)
);
}

personal showErrorSnackBar() {
this.snackBar.open(‘There was an issue creating your deal with.’, ‘Shut’, { length: 8000 });
}

personal showSuccessSnackBar() {
this.snackBar.open(‘Transport deal with efficiently added. Redirecting…’, ‘Shut’, { length: 3000 });

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

Right here is the template and its styling will be discovered right here.

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

The transport deal with web page will appear like this.

Transport Strategies Element

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

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

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

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

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

constructor(
personal orders: OrderService,
personal dlts: DeliveryLeadTimeService,
personal cart: CartService,
personal router: Router,
personal fb: FormBuilder,
personal shipments: ShipmentService,
personal 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 => ”);
merchandise.imageUrl = li.imageUrl;
merchandise.identify = li.identify;
return merchandise;
);
}

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

return cargo;
});
},
err => this.router.navigateByUrl(‘/error’)
);
}

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

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

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

personal findLocationLeadTime(instances: DeliveryLeadTime[], cargo: Cargo): DeliveryLeadTime[] {
return instances.filter((dlTime) => dlTime?.stockLocation?.id == cargo?.stockLocation?.id);
}

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

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

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

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

Funds Element

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

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

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

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

constructor(
personal orders: OrderService,
personal cart: CartService,
personal router: Router,
personal funds: PaypalPaymentService
) { }

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

this.orders.getOrder(orderId, GetOrderParams.paymentSource)
.pipe(
concatMap((order: Order) => {
const paymentSourceId = order.paymentSource?.id;

const paymentMethod = order.availablePaymentMethods?.filter(
(technique) => technique.paymentSourceType == ‘paypal_payments’
)[0];

return iif(
() => paymentSourceId ? true : false,
this.funds.getPaypalPayment(paymentSourceId || ”),
this.orders.updateOrder({
id: orderId,
paymentMethodId: paymentMethod?.id
}, [UpdateOrderParams.paymentMethod])
.pipe(concatMap(
order => this.funds.createPaypalPayment({
orderId: orderId,
cancelUrl: ${setting.clientUrl}/cancel-payment,
returnUrl: ${setting.clientUrl}/place-order
})
))
);
}))
.subscribe(
paypalPayment => this.approvalUrl = paypalPayment?.approvalUrl || ”,
err => this.router.navigateByUrl(‘/error’)
);
}

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

Right here is its template.

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

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

Cancel Fee Element

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

<app-simple-page title=”Fee cancelled” subtitle=”Your Paypal fee has been cancelled” icon=”money_off” buttonText=”GO TO HOME” [centerText]=”true” route=”/”></app-simple-page>

Right here’s a screenshot of the web page.

Place Order Element

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

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

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

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

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

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

return iif(
() => payerId.size > 0,
this.orders.getOrder(orderId, GetOrderParams.paymentSource)
.pipe(
concatMap(order => )
)
);
}))
.subscribe(
() => this.disableButton = false,
() => this.router.navigateByUrl(‘/error’)
);
}

placeOrder() {
this.disableButton = true;

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

Right here is the template and its related styling.

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

Here’s a screenshot of the web page.

App Module

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

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

App Element

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

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

Conclusion

On this article, we’ve coated how you would create an e-commerce Angular 11 app with Commerce Layer and Paypal. We’ve additionally touched on how one can construction the app and the way you would interface with an e-commerce API.

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

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

    About Marketing Solution Australia

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment