Cum să construiți un site de comerț electronic cu Angular 11, Commerce Layer și Paypal

Publicat: 2022-03-10
Rezumat rapid ↬ Deținerea unui magazin de comerț electronic este crucială pentru orice proprietar de magazin, deoarece din ce în ce mai mulți clienți apelează la cumpărături online. În acest tutorial, vom parcurge cum să creați un site de comerț electronic cu Angular 11. Site-ul va folosi Commerce Layer ca API de comerț electronic fără cap și va folosi Paypal pentru a procesa plăți.

În zilele noastre este esențial să ai o prezență online atunci când conduci o afacere. Se fac mult mai multe cumpărături online decât în ​​anii precedenți. A avea un magazin de comerț electronic le permite proprietarilor de magazine să deschidă alte fluxuri de venituri de care nu le-ar putea profita doar cu un magazin de cărămidă și mortar. Cu toate acestea, alți proprietari de magazine își desfășoară afacerile online în întregime fără prezență fizică. Acest lucru face ca deținerea unui magazin online este crucială.

Site-uri precum Etsy, Shopify și Amazon facilitează configurarea unui magazin destul de rapid, fără a fi nevoie să vă faceți griji cu privire la dezvoltarea unui site. Cu toate acestea, pot exista situații în care proprietarii de magazine ar putea dori o experiență personalizată sau poate economisi din costul deținerii unui magazin pe unele dintre aceste platforme.

Platformele API de comerț electronic fără cap oferă backend-uri cu care site-urile magazinelor pot interfața. Ei gestionează toate procesele și datele legate de magazin, cum ar fi clienți, comenzi, expedieri, plăți și așa mai departe. Tot ceea ce este necesar este un frontend pentru a interacționa cu aceste informații. Acest lucru le oferă proprietarilor multă flexibilitate atunci când vine vorba de a decide cum își vor experimenta clienții magazinul online și cum aleg să-l administreze.

În acest articol, vom aborda cum să construim un magazin de comerț electronic folosind Angular 11. Vom folosi Commerce Layer ca API de comerț electronic fără cap. Deși pot exista o mulțime de modalități de procesare a plăților, vă vom demonstra cum să folosiți doar una, Paypal.

  • Vizualizați codul sursă pe GitHub →

Cerințe preliminare

Înainte de a construi aplicația, trebuie să aveți instalat Angular CLI. Îl vom folosi pentru a inițializa și a monta aplicația. Dacă nu îl aveți încă instalat, îl puteți obține prin npm.

 npm install -g @angular/cli

Veți avea nevoie și de un cont de dezvoltator Commerce Layer. Folosind contul de dezvoltator, va trebui să creați o organizație de testare și să o introduceți cu date de testare. Însămânțarea facilitează dezvoltarea mai întâi a aplicației, fără să vă faceți griji cu privire la datele pe care va trebui să le utilizați. Vă puteți crea un cont la acest link și o organizație aici.

Tabloul de bord al organizațiilor contului de dezvoltator Commerce Layer
Tabloul de bord al organizațiilor contului de dezvoltator Commerce Layer unde vă adăugați organizația. (Previzualizare mare)
Formular de creare a organizațiilor Commerce Layer
Bifați caseta Seed with test data atunci când creați o nouă organizație. (Previzualizare mare)

În cele din urmă, veți avea nevoie de un cont Paypal Sandbox. Având acest tip de cont, ne va permite să testăm tranzacții între companii și utilizatori fără a risca banii efectivi. Puteți crea unul aici. Un cont sandbox are deja un cont de afaceri de testare și un cont personal de testare creat pentru el.

Mai multe după săritură! Continuați să citiți mai jos ↓

Nivelul de comerț și configurația Paypal

Pentru a face posibile plățile Paypal Sandbox pe Commerce Layer, va trebui să configurați cheile API. Mergeți la prezentarea generală a conturilor contului dvs. de dezvoltator Paypal. Selectați un cont de afaceri și în fila Acreditări API a detaliilor contului, veți găsi aplicația implicită în Aplicații REST .

Fila Acreditări API din fereastra pop-up pentru detaliile contului de afaceri Paypal Sandbox
Unde puteți găsi aplicația REST implicită în fereastra pop-up pentru detaliile contului de afaceri Paypal. (Previzualizare mare)
Prezentare generală a aplicației implicite în setările contului de afaceri Paypal Sandbox
Prezentare generală a aplicației implicite în setările contului de afaceri Paypal Sandbox de unde puteți obține ID-ul și secretul clientului REST API. (Previzualizare mare)

Pentru a asocia contul dvs. de afaceri Paypal cu organizația dvs. Commerce Layer, accesați tabloul de bord al organizației dvs. Aici veți adăuga un gateway de plată Paypal și o metodă de plată Paypal pentru diferitele dumneavoastră piețe. Sub Setări > Plăți , selectați Gateway de plată > Paypal și adăugați ID-ul și secretul dvs. de client Paypal.

Noul tablou de bord Payments Gateway pe stratul Commerce
Unde în tabloul de bord Commerce Layer pentru a crea un gateway de plăți Paypal. (Previzualizare mare)

După crearea gateway-ului, va trebui să creați o metodă de plată Paypal pentru fiecare piață pe care o vizați pentru a face Paypal disponibil ca opțiune. Veți face acest lucru în Setări > Plăți > Metode de plată > Metodă nouă de plată .

Tabloul de bord Metode de plată pe Stratul Commerce
Unde în tabloul de bord Commerce Layer pentru a crea o metodă de plată Paypal. (Previzualizare mare)

O notă despre rutele folosite

Commerce Layer oferă o rută pentru autentificare și un alt set diferit de rute pentru API-ul lor. Ruta lor /oauth/token schimbă acreditările pentru un token. Acest simbol este necesar pentru a-și accesa API-ul. Restul rutelor API au modelul /api/:resource .

Scopul acestui articol acoperă doar porțiunea de front-end a acestei aplicații. Am optat să stochez partea de server de token-uri, să folosesc sesiuni pentru a urmări proprietatea și să ofer cookie-uri numai http cu un ID de sesiune clientului. Acest lucru nu va fi tratat aici, deoarece este în afara domeniului de aplicare al acestui articol. Cu toate acestea, rutele rămân aceleași și corespund exact API-ului Commerce Layer. Deși, există câteva rute personalizate care nu sunt disponibile din API-ul Commerce Layer pe care le vom folosi. Acestea se ocupă în principal de gestionarea sesiunilor. Voi sublinia acestea pe măsură ce ajungem la ele și voi descrie cum puteți obține un rezultat similar.

O altă inconsecvență pe care o puteți observa este că corpurile de solicitare diferă de ceea ce necesită API-ul Commerce Layer. Deoarece cererile sunt transmise către un alt server pentru a fi populate cu un token, am structurat corpurile diferit. Acest lucru a fost pentru a facilita trimiterea cererilor. Ori de câte ori există neconcordanțe în organele de solicitare, acestea vor fi semnalate în servicii.

Deoarece acest lucru este în afara domeniului de aplicare, va trebui să decideți cum să stocați jetoanele în siguranță. De asemenea, va trebui să modificați ușor corpurile de solicitare pentru a se potrivi exact cu ceea ce necesită API-ul Commerce Layer. Când există o inconsecvență, voi face link la referința API și la ghiduri care detaliază cum să structurați corect corpul.

Structura aplicației

Pentru a organiza aplicația, o vom împărți în patru părți principale. O descriere mai bună a ceea ce face fiecare dintre module este dată în secțiunile corespunzătoare:

  1. modulul de bază,
  2. modulul de date,
  3. modulul partajat,
  4. modulele de caracteristici.

Modulele de caracteristici vor grupa paginile și componentele asociate împreună. Vor fi patru module de caracteristici:

  1. modulul de autentificare,
  2. modulul de produs,
  3. modulul cărucior,
  4. modulul de casă.

Pe măsură ce ajungem la fiecare modul, voi explica care este scopul său și îi voi descompune conținutul.

Mai jos este un arbore al folderului src/app și unde se află fiecare modul.

 src ├── app │ ├── core │ ├── data │ ├── features │ │ ├── auth │ │ ├── cart │ │ ├── checkout │ │ └── products └── shared

Generarea aplicației și adăugarea de dependențe

Vom începe prin a genera aplicația. Organizația noastră se va numi The LIme Brand și va avea date de testare deja însămânțate de Commerce Layer.

 ng new lime-app

Vom avea nevoie de câteva dependențe. Material în principal unghiular și până la distrugere. Materialul Angular va oferi componente și stil. Until Destroy se dezabonează automat de la observabile atunci când componentele sunt distruse. Pentru a le instala, rulați:

 npm install @ngneat/until-destroy ng add @angular/material

Active

Când adăugați adrese la Commerce Layer, trebuie utilizat un cod de țară alfa-2. Vom adăuga un fișier json care conține aceste coduri în folderul assets la assets/json/country-codes.json . Puteți găsi acest fișier legat aici.

Stiluri

Componentele pe care le vom crea împărtășesc un stil global. Le vom plasa în styles.css , care poate fi găsit la acest link.

Mediu inconjurator

Configurația noastră va consta din două câmpuri. apiUrl care ar trebui să indice către API-ul Commerce Layer. apiUrl este folosit de serviciile pe care le vom crea pentru a prelua date. clientUrl ar trebui să fie domeniul pe care rulează aplicația. Folosim acest lucru atunci când setăm adrese URL de redirecționare pentru Paypal. Puteți găsi acest fișier la acest link.

Modul partajat

Modulul partajat va conține servicii, conducte și componente partajate între celelalte module.

 ng gm shared

Este format din trei componente, o conductă și două servicii. Iată cum va arăta.

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

Vom folosi, de asemenea, modulul partajat pentru a exporta unele componente utilizate în mod obișnuit de Material Angular. Acest lucru face mai ușor să le folosiți din cutie, în loc să importați fiecare componentă în diferite module. Iată ce va conține shared.module.ts .

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

Componente

Articol Cantitate Componentă

Această componentă stabilește cantitatea de articole atunci când le adaugă în coș. Va fi folosit în modulele de coș și produse. Un selector de materiale ar fi fost o alegere ușoară în acest scop. Cu toate acestea, stilul materialului selectat nu se potrivea cu intrările de material utilizate în toate celelalte forme. Un meniu de materiale arăta foarte asemănător cu intrările materiale utilizate. Așa că am decis să creez o componentă selectată cu ea.

 ng gc shared/components/item-quantity

Componenta va avea trei proprietăți de intrare și o proprietate de ieșire. quantity setează cantitatea inițială de articole, maxValue indică numărul maxim de articole care pot fi selectate dintr-o singură mișcare, iar disabled indică dacă componenta trebuie dezactivată sau nu. setQuantityEvent este declanșat atunci când este selectată o cantitate.

Când componenta este inițializată, vom seta valorile care apar în meniul material. Există, de asemenea, o metodă numită setQuantity care va emite evenimente setQuantityEvent .

Acesta este fișierul component.

 @Component({ selector: 'app-item-quantity', templateUrl: './item-quantity.component.html', styleUrls: ['./item-quantity.component.css'] }) export class ItemQuantityComponent implements OnInit { @Input() quantity: number = 0; @Input() maxValue?: number = 0; @Input() disabled?: boolean = false; @Output() setQuantityEvent = new EventEmitter<number>(); values: number[] = []; constructor() { } ngOnInit() { if (this.maxValue) { for (let i = 1; i <= this.maxValue; i++) { this.values.push(i); } } } setQuantity(value: number) { this.setQuantityEvent.emit(value); } }

Acesta este șablonul său.

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

Iată stilul său.

 button { margin: 3px; }

Componenta de titlu

Această componentă se dublează ca titlu stepper, precum și ca titlu simplu pe unele pagini mai simple. Deși Angular Material oferă o componentă stepper, nu a fost cea mai potrivită pentru un proces de plată destul de lung, nu a fost la fel de receptiv pe afișaje mai mici și a necesitat mult mai mult timp pentru implementare. Cu toate acestea, un titlu mai simplu ar putea fi reutilizat ca un indicator pas cu pas și poate fi util pe mai multe pagini.

 ng gc shared/components/title

Componenta are patru proprietăți de intrare: un title , un subtitle , un număr ( no ) și centerText , pentru a indica dacă trebuie centrat textul componentei.

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

Mai jos este șablonul acestuia. Puteți găsi stilul său legat aici.

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

Componentă pagină simplă

Există mai multe cazuri în care un titlu, o pictogramă și un buton au fost tot ceea ce era necesar pentru o pagină. Acestea includ o pagină 404, o pagină de coș goală, o pagină de eroare, o pagină de plată și o pagină de plasare a comenzii. Acesta este scopul pe care îl va servi componenta simplă a paginii. Când se face clic pe butonul de pe pagină, acesta fie va redirecționa către o rută, fie va efectua o acțiune ca răspuns la un buttonEvent .

Să-l facă:

 ng gc shared/components/simple-page

Acesta este fișierul său component.

 @Component({ selector: 'app-simple-page', templateUrl: './simple-page.component.html', styleUrls: ['./simple-page.component.css'] }) export class SimplePageComponent { @Input() title: string = ''; @Input() subtitle?: string; @Input() number?: string; @Input() icon?: string; @Input() buttonText: string = ''; @Input() centerText?: boolean = false; @Input() buttonDisabled?: boolean = false; @Input() route?: string | undefined; @Output() buttonEvent = new EventEmitter(); constructor(private router: Router) { } buttonClicked() { if (this.route) { this.router.navigateByUrl(this.route); } else { this.buttonEvent.emit(); } } }

Și șablonul său conține:

 <div> <app-title no="{{number}}" title="{{title}}" subtitle="{{subtitle}}" [centerText]="centerText"></app-title> <div *ngIf="icon"> <mat-icon color="primary" class="icon">{{icon}}</mat-icon> </div> <button mat-flat-button color="primary" (click)="buttonClicked()" [disabled]="buttonDisabled"> {{buttonText}} </button> </div>

Modelul său poate fi găsit aici.

Conducte

Word Wrap Pipe

Numele unor produse și alte tipuri de informații afișate pe site sunt foarte lungi. În unele cazuri, obținerea acestor propoziții lungi pentru a se încheia în componente materiale este o provocare. Deci vom folosi această țeavă pentru a tăia propozițiile la o lungime specificată și pentru a adăuga elipse la sfârșitul rezultatului.

Pentru a-l crea, rulați:

 ng g pipe shared/pipes/word-wrap

Acesta va conține:

 import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'wordWrap' }) export class WordWrapPipe implements PipeTransform { transform(value: string, length: number): string { return `${value.substring(0, length)}...`; } }

Servicii

Serviciul de gestionare a erorilor HTTP

Există un număr destul de mare de servicii http în acest proiect. Crearea unui handler de erori pentru fiecare metodă este repetitivă. Deci, crearea unui singur handler care poate fi folosit de toate metodele are sens. Managerul de erori poate fi folosit pentru a formata o eroare și, de asemenea, pentru a transmite erorile către alte platforme externe de înregistrare.

Generați-l rulând:

 ng gs shared/services/http-error-handler

Acest serviciu va conține o singură metodă. Metoda va formata mesajul de eroare care urmează să fie afișat, în funcție de faptul că este o eroare de client sau de server. Cu toate acestea, există loc de îmbunătățit în continuare.

 @Injectable({ providedIn: 'root' }) export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = `Client-side error: ${err.error.message}`; } else { displayMessage = `Server-side error: ${err.message}`; } return throwError(displayMessage); } } @Injectable({ providedIn: 'root' }) export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = `Client-side error: ${err.error.message}`; } else { displayMessage = `Server-side error: ${err.message}`; } return throwError(displayMessage); } }

Serviciul de depozitare local

Vom folosi stocarea locală pentru a ține evidența numărului de articole dintr-un coș. De asemenea, este util să stocați aici ID-ul unei comenzi. O comandă corespunde unui coș pe Stratul Commerce.

Pentru a genera serviciul de stocare local, rulați:

 ng gs shared/services/local-storage

Serviciul va conține patru metode pentru a adăuga, șterge și obține articole din stocarea locală și o alta pentru a-l șterge.

 import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class LocalStorageService { constructor() { } addItem(key: string, value: string) { localStorage.setItem(key, value); } deleteItem(key: string) { localStorage.removeItem(key); } getItem(key: string): string | null { return localStorage.getItem(key); } clear() { localStorage.clear(); } }

Modulul de date

Acest modul este responsabil pentru preluarea și gestionarea datelor. Este ceea ce vom folosi pentru a obține datele consumate de aplicația noastră. Mai jos este structura sa:

 src/app/data ├── data.module.ts ├── models └── services

Pentru a genera modulul rulați:

 ng gm data

Modele

Modelele definesc modul în care sunt structurate datele pe care le consumăm din API. Vom avea 16 declarații de interfață. Pentru a le crea rulați:

 for model in \ address cart country customer-address \ customer delivery-lead-time line-item order \ payment-method payment-source paypal-payment \ price shipment shipping-method sku stock-location; \ do ng g interface "data/models/${model}"; done

Următorul tabel face legătura cu fiecare fișier și oferă o descriere a fiecărei interfețe.

Interfață Descriere
Abordare Reprezintă o adresă generală.
Cart Versiunea pentru client a unei comenzi care urmărește numărul de produse pe care un client intenționează să le cumpere.
Țară Cod de țară alfa-2.
Adresa Clientului O adresă asociată unui client.
Client Un utilizator înregistrat.
Timp total de livrare Reprezintă timpul necesar pentru livrarea unei expedieri.
Element rând Un produs detaliat adăugat în coș.
Ordin Un coș de cumpărături sau o colecție de articole rând.
Modalitate de plată Un tip de plată pus la dispoziția unei comenzi.
Sursa de plată O plată asociată cu o comandă.
Plata Paypal O plată efectuată prin Paypal
Preț Preț asociat cu un SKU.
Expediere Colectare de articole expediate împreună.
Metodă de livrare Metoda prin care este expediat un pachet.
SKU O unitate unică de stocare.
Locația stocului Locație care conține inventarul SKU.

Servicii

Acest folder conține serviciile care creează, preiau și manipulează datele aplicației. Vom crea 11 servicii aici.

 for service in \ address cart country customer-address \ customer delivery-lead-time line-item \ order paypal-payment shipment sku; \ do ng gs "data/services/${service}"; done

Serviciul Adresă

Acest serviciu creează și preia adrese. Este important atunci când creați și atribuiți comenzilor adrese de expediere și de facturare. Are două metode. Una pentru a crea o adresă și alta pentru a prelua una.

Ruta folosită aici este /api/addresses . Dacă intenționați să utilizați direct API-ul Commerce Layer, asigurați-vă că structurați datele așa cum este demonstrat în acest exemplu.

 @Injectable({ providedIn: 'root' }) export class AddressService { private url: string = `${environment.apiUrl}/api/addresses`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createAddress(address: Address): Observable<Address> { return this.http.post<Address>(this.url, address) .pipe(catchError(this.eh.handleError)); } getAddress(id: string): Observable<Address> { return this.http.get<Address>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

Service cărucioare

Coșul este responsabil pentru menținerea cantității de articole adăugate și a codului comenzii. Efectuarea de apeluri API pentru a obține numărul de articole dintr-o comandă de fiecare dată când este creat un nou element rând poate fi costisitoare. În schimb, am putea folosi doar stocarea locală pentru a menține numărul pe client. Acest lucru elimină necesitatea de a face preluări inutile de comandă de fiecare dată când un articol este adăugat în coș.

De asemenea, folosim acest serviciu pentru a stoca ID-ul comenzii. Un coș corespunde unei comenzi pe Commerce Layer. Odată ce primul articol este adăugat în coș, se creează o comandă. Trebuie să păstrăm acest ID de comandă pentru a-l putea prelua în timpul procesului de finalizare a comenzii.

În plus, avem nevoie de o modalitate de a comunica antetului că un articol a fost adăugat în coș. Antetul conține butonul coș și afișează cantitatea de articole din acesta. Vom folosi un observabil al unui BehaviorSubject cu valoarea curentă a coșului. Antetul se poate abona la aceasta și poate urmări modificările în valoarea coșului.

În cele din urmă, odată ce o comandă a fost finalizată, valoarea coșului trebuie să fie ștearsă. Acest lucru asigură că nu există confuzie atunci când creați comenzi ulterioare mai noi. Valorile care au fost stocate sunt șterse odată ce comanda curentă este marcată ca fiind plasată.

Vom realiza toate acestea folosind serviciul de stocare local creat mai devreme.

 @Injectable({ providedIn: 'root' }) export class CartService { private cart = new BehaviorSubject({ orderId: this.orderId, itemCount: this.itemCount }); cartValue$ = this.cart.asObservable(); constructor(private storage: LocalStorageService) { } get orderId(): string { const id = this.storage.getItem('order-id'); return id ? id : ''; } set orderId(id: string) { this.storage.addItem('order-id', id); this.cart.next({ orderId: id, itemCount: this.itemCount }); } get itemCount(): number { const itemCount = this.storage.getItem('item-count'); return itemCount ? parseInt(itemCount) : 0; } set itemCount(amount: number) { this.storage.addItem('item-count', amount.toString()); this.cart.next({ orderId: this.orderId, itemCount: amount }); } incrementItemCount(amount: number) { this.itemCount = this.itemCount + amount; } decrementItemCount(amount: number) { this.itemCount = this.itemCount - amount; } clearCart() { this.storage.deleteItem('item-count'); this.cart.next({ orderId: '', itemCount: 0 }); } }

Serviciul la țară

Când adăugați adrese în Commerce Layer, codul de țară trebuie să fie un cod alfa 2. Acest serviciu citește un fișier json care conține aceste coduri pentru fiecare țară și îl returnează în metoda getCountries .

 @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } } @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } } @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } }

Serviciul Adresă Clienți

Acest serviciu este folosit pentru a asocia adrese cu clienții. De asemenea, preia o anumită sau toate adresele legate de un client. Este folosit atunci când clientul își adaugă adresele de expediere și de facturare la comanda sa. Metoda createCustomer creează un client, getCustomerAddresses primește toate adresele unui client și getCustomerAddress primește una specifică.

Când creați o adresă de client, asigurați-vă că structurați corpul postării conform acestui exemplu.

 @Injectable({ providedIn: 'root' }) export class CustomerAddressService { private url: string = `${environment.apiUrl}/api/customer_addresses`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomerAddress(addressId: string, customerId: string): Observable<CustomerAddress> { return this.http.post<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)); } }

Serviciu clienți

Clienții sunt creați și informațiile lor sunt preluate folosind acest serviciu. Când un utilizator se înscrie, acesta devine client și este creat folosind createCustomerMethod . getCustomer returnează clientul asociat cu un anumit ID. getCurrentCustomer returnează clientul conectat în prezent.

Când creați un client, structurați datele astfel. Puteți adăuga numele și prenumele lor la metadate, așa cum se arată în atributele acestora.

Ruta /api/customers/current nu este disponibilă în Commerce Layer. Deci, va trebui să vă dați seama cum să obțineți clientul conectat în prezent.

 @Injectable({ providedIn: 'root' }) export class CustomerService { private url: string = `${environment.apiUrl}/api/customers`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomer(email: string, password: string, firstName: string, lastName: string): Observable<Customer> { return this.http.post<Customer>(this.url, { email: email, password: password, firstName: firstName, lastName: lastName }) .pipe(catchError(this.eh.handleError)); } getCurrentCustomer(): Observable<Customer> { return this.http.get<Customer>(`${this.url}/current`) .pipe(catchError(this.eh.handleError)); } getCustomer(id: string): Observable<Customer> { return this.http.get<Customer>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

Serviciu de timp de livrare

Acest serviciu returnează informații despre termenele de livrare din diferite locații de stoc.

 @Injectable({ providedIn: 'root' }) export class DeliveryLeadTimeService { private url: string = `${environment.apiUrl}/api/delivery_lead_times`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getDeliveryLeadTimes(): Observable<DeliveryLeadTime[]> { return this.http.get<DeliveryLeadTime[]>(this.url) .pipe(catchError(this.eh.handleError)); } }

Serviciu articol rând

Articolele adăugate în coș sunt gestionate de acest serviciu. Cu acesta, puteți crea un articol în momentul în care este adăugat în coș. Informațiile despre un articol pot fi, de asemenea, preluate. Articolul poate fi, de asemenea, actualizat atunci când cantitatea sa se modifică sau șters atunci când este scos din coș.

Când creați articole sau le actualizați, structurați corpul cererii așa cum se arată în acest exemplu.

 @Injectable({ providedIn: 'root' }) export class LineItemService { private url: string = `${environment.apiUrl}/api/line_items`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createLineItem(lineItem: LineItem): Observable<LineItem> { return this.http.post<LineItem>(this.url, lineItem) .pipe(catchError(this.eh.handleError)); } getLineItem(id: string): Observable<LineItem> { return this.http.get<LineItem>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updateLineItem(id: string, quantity: number): Observable<LineItem> { return this.http.patch<LineItem>(`${this.url}/${id}`, { quantity: quantity }) .pipe(catchError(this.eh.handleError)); } deleteLineItem(id: string): Observable<LineItem> { return this.http.delete<LineItem>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

Serviciul de Comandă

Similar cu serviciul de articole rând, serviciul de comandă vă permite să creați, să actualizați, să ștergeți sau să obțineți o comandă. În plus, puteți alege să obțineți livrările asociate unei comenzi separat, folosind metoda getOrderShipments . Acest serviciu este utilizat intens pe parcursul procesului de finalizare a comenzii.

Există diferite tipuri de informații despre o comandă care sunt solicitate pe parcursul plății. Deoarece poate fi costisitor să preluăm o comandă întreagă și relațiile acesteia, specificăm ce dorim să obținem dintr-o comandă folosind GetOrderParams . Echivalentul acestuia în API-ul CL este parametrul de interogare include în care enumerați relațiile de ordine care trebuie incluse. Puteți verifica ce câmpuri trebuie incluse pentru rezumatul coșului aici și pentru diferitele etape de finalizare a achiziției aici.

În același mod, atunci când actualizăm o comandă, folosim UpdateOrderParams pentru a specifica câmpurile de actualizare. Acest lucru se datorează faptului că în serverul care populează jetonul, sunt efectuate unele operații suplimentare în funcție de ce câmp este actualizat. Cu toate acestea, dacă faceți solicitări directe către CL API, nu trebuie să specificați acest lucru. Puteți renunța la el, deoarece API-ul CL nu vă solicită să le specificați. Deși, organismul de solicitare ar trebui să semene cu acest exemplu.

 @Injectable({ providedIn: 'root' }) export class OrderService { private url: string = `${environment.apiUrl}/api/orders`; constructor( private http: HttpClient, private eh: HttpErrorHandler) { } createOrder(): Observable<Order> { return this.http.post<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: { 'field': updateParams } } ) .pipe(catchError(this.eh.handleError)); } getOrderShipments(id: string): Observable<Shipment[]> { return this.http.get<Shipment[]>(`${this.url}/${id}/shipments`) .pipe(catchError(this.eh.handleError)); } }

Serviciu de plată Paypal

Acest serviciu este responsabil pentru crearea și actualizarea plăților Paypal pentru comenzi. În plus, putem obține o plată Paypal având în vedere id-ul acesteia. Corpul postării ar trebui să aibă o structură similară cu acest exemplu atunci când creați o plată Paypal.

 @Injectable({ providedIn: 'root' }) export class PaypalPaymentService { private url: string = `${environment.apiUrl}/api/paypal_payments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createPaypalPayment(payment: PaypalPayment): Observable<PaypalPayment> { return this.http.post<PaypalPayment>(this.url, payment) .pipe(catchError(this.eh.handleError)); } getPaypalPayment(id: string): Observable<PaypalPayment> { return this.http.get<PaypalPayment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updatePaypalPayment(id: string, paypalPayerId: string): Observable<PaypalPayment> { return this.http.patch<PaypalPayment>( `${this.url}/${id}`, { paypalPayerId: paypalPayerId } ) .pipe(catchError(this.eh.handleError)); } }

Serviciul de expediere

Acest serviciu primește o expediere sau o actualizează având în vedere id-ul său. Corpul solicitării unei actualizări de expediere ar trebui să arate similar cu acest exemplu.

 @Injectable({ providedIn: 'root' }) export class ShipmentService { private url: string = `${environment.apiUrl}/api/shipments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getShipment(id: string): Observable<Shipment> { return this.http.get<Shipment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updateShipment(id: string, shippingMethodId: string): Observable<Shipment> { return this.http.patch<Shipment>( `${this.url}/${id}`, { shippingMethodId: shippingMethodId } ) .pipe(catchError(this.eh.handleError)); } }

Serviciul SKU

Serviciul SKU primește produse din magazin. Dacă sunt preluate mai multe produse, acestea pot fi paginate și pot avea o dimensiune de pagină setată. Mărimea paginii și numărul paginii ar trebui setate ca parametri de interogare, ca în acest exemplu, dacă faceți solicitări directe către API. Un singur produs poate fi, de asemenea, preluat având în vedere id-ul său.

 @Injectable({ providedIn: 'root' }) export class SkuService { private url: string = `${environment.apiUrl}/api/skus`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getSku(id: string): Observable<Sku> { return this.http.get<Sku>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } getSkus(page: number, pageSize: number): Observable<Sku[]> { return this.http.get<Sku[]>( this.url, { params: { 'page': page.toString(), 'pageSize': pageSize.toString() } }) .pipe(catchError(this.eh.handleError)); } }

Modulul de bază

Modulul de bază conține tot ce este central și comun în cadrul aplicației. Acestea includ componente precum antetul și pagini precum pagina 404. Aici se încadrează și serviciile responsabile pentru autentificare și gestionarea sesiunilor, precum și interceptori și paznici la nivelul aplicației.

Arborele modulului de bază va arăta astfel.

 src/app/core ├── components │ ├── error │ │ ├── error.component.css │ │ ├── error.component.html │ │ └── error.component.ts │ ├── header │ │ ├── header.component.css │ │ ├── header.component.html │ │ └── header.component.ts │ └── not-found │ ├── not-found.component.css │ ├── not-found.component.html │ └── not-found.component.ts ├── core.module.ts ├── guards │ └── empty-cart.guard.ts ├── interceptors │ └── options.interceptor.ts └── services ├── authentication.service.ts ├── header.service.ts └── session.service.ts

Pentru a genera modulul și conținutul acestuia, rulați:

 ng gm core ng gg core/guards/empty-cart ng gs core/header/header ng g interceptor core/interceptors/options for comp in header error not-found; do ng gc "core/${comp}"; done for serv in authentication session; do ng gs "core/authentication/${serv}"; done

Fișierul modulului de bază ar trebui să fie așa. Rețineți că rutele au fost înregistrate pentru NotFoundComponent și 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 { }

Servicii

Dosarul de servicii conține serviciile de autentificare, sesiune și antet.

Serviciul de autentificare

AuthenticationService vă permite să achiziționați token-uri client și client. Aceste jetoane sunt folosite pentru a accesa restul rutelor API-ului. Jetoanele de client sunt returnate atunci când un utilizator schimbă un e-mail și o parolă pentru acesta și are o gamă mai largă de permisiuni. Token-urile client sunt emise fără a avea nevoie de acreditări și au permisiuni mai restrânse.

getClientSession primește un token client. login primește un token de client. Ambele metode creează și o sesiune. Corpul unei cereri de token client ar trebui să arate astfel și cel al unui token client ca acesta.

 @Injectable({ providedIn: 'root' }) export class AuthenticationService { private url: string = `${environment.apiUrl}/oauth/token`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getClientSession(): Observable<object> { return this.http.post<object>( this.url, { grantType: 'client_credentials' }) .pipe(catchError(this.eh.handleError)); } login(email: string, password: string): Observable<object> { return this.http.post<object>( this.url, { username: email, password: password, grantType: 'password' }) .pipe(catchError(this.eh.handleError)); } }

Session Service

The SessionService is responsible for session management. The service will contain an observable from a BehaviorSubject called loggedInStatus to communicate whether a user is logged in. setLoggedInStatus sets the value of this subject, true for logged in, and false for not logged in. isCustomerLoggedIn makes a request to the server to check if the user has an existing session. logout destroys the session on the server. The last two methods access routes that are unique to the server that populates the request with a token. They are not available from Commerce Layer. You'll have to figure out how to implement them.

 @Injectable({ providedIn: 'root' }) export class SessionService { private url: string = `${environment.apiUrl}/session`; private isLoggedIn = new BehaviorSubject(false); loggedInStatus = this.isLoggedIn.asObservable(); constructor(private http: HttpClient, private eh: HttpErrorHandler) { } setLoggedInStatus(status: boolean) { this.isLoggedIn.next(status); } isCustomerLoggedIn(): Observable<{ message: string }> { return this.http.get<{ message: string }>(`${this.url}/customer/status`) .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 communicate whether the cart, login, and logout buttons should be shown in the header. These buttons are hidden on the login and signup pages but present on all other pages to prevent confusion. We'll use an observable from a BehaviourSubject called showHeaderButtons that shares this. We'll also have a setHeaderButtonsVisibility method to set this value.

 @Injectable({ providedIn: 'root' }) export class HeaderService { private headerButtonsVisibility = new BehaviorSubject(true); showHeaderButtons = this.headerButtonsVisibility.asObservable(); constructor() { } setHeaderButtonsVisibility(visible: boolean) { this.headerButtonsVisibility.next(visible); } }

Componente

Error Component

This component is used as an error page. It is useful in instances when server requests fail and absolutely no data is displayed on a page. Instead of showing a blank page, we let the user know that a problem occurred. Below is it's template.

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

This is what the component will look like.

Screenshot of error page
Screenshot of error page. (Previzualizare mare)

Not Found Component

This is a 404 page that the user gets redirected to when they request a route not available on the router. Only its template is modified.

 <app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>
Screenshot of 404 page
Screenshot of 404 page. (Previzualizare mare)

Header Component

The HeaderComponent is basically the header displayed at the top of a page. It will contain the app title, the cart, login, and logout buttons.

When this component is initialized, a request is made to check whether the user has a current session. This happens when subscribing to this.session.isCustomerLoggedIn() . We subscribe to this.session.loggedInStatus to check if the user logs out throughout the life of the app. The this.header.showHeaderButtons subscription decides whether to show all the buttons on the header or hide them. this.cart.cartValue$ gets the count of items in the cart.

Există o metodă de logout care distruge sesiunea unui utilizator și le atribuie un token client. Un token client este alocat deoarece sesiunea care menține token-ul clientului este distrusă și este încă necesar un token pentru fiecare solicitare API. Un snackbar material comunică utilizatorului dacă sesiunea sa a fost distrusă cu succes sau nu.

Folosim @UntilDestroy({ checkProperties: true }) pentru a indica faptul că toate abonamentele ar trebui să fie dezabonate automat din momentul în care componenta este distrusă.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'] }) export class HeaderComponent implements OnInit { cartAmount: number = 0; isLoggedIn: boolean = false; showButtons: boolean = true; constructor( private session: SessionService, private snackBar: MatSnackBar, private cart: CartService, private header: HeaderService, private auth: AuthenticationService ) { } ngOnInit() { this.session.isCustomerLoggedIn() .subscribe( () => { this.isLoggedIn = true; this.session.setLoggedInStatus(true); } ); this.session.loggedInStatus.subscribe(status => this.isLoggedIn = status); this.header.showHeaderButtons.subscribe(visible => this.showButtons = visible); this.cart.cartValue$.subscribe(cart => this.cartAmount = cart.itemCount); } logout() { concat( this.session.logout(), this.auth.getClientSession() ).subscribe( () => { this.snackBar.open('You have been logged out.', 'Close', { duration: 4000 }); this.session.setLoggedInStatus(false); }, err => this.snackBar.open('There was a problem logging you out.', 'Close', { duration: 4000 }) ); } }

Mai jos este șablonul de antet și aici este legat de stilul acestuia.

 <div> <div routerLink="/"> <h1><span>Lime</span><span>Store</span></h1> </div> <div> <div *ngIf="showButtons"> <button mat-icon-button color="primary" aria-label="shopping cart"> <mat-icon [matBadge]="cartAmount" matBadgeColor="accent" aria-label="shopping cart" routerLink="/cart">shopping_cart</mat-icon> </button> <button mat-icon-button color="primary" aria-label="login" *ngIf="!isLoggedIn"> <mat-icon aria-label="login" matTooltip="login" routerLink="/login">login</mat-icon> </button> <button mat-icon-button color="primary" aria-label="logout" *ngIf="isLoggedIn" (click)="logout()"> <mat-icon aria-label="logout" matTooltip="logout">logout</mat-icon> </button> </div> </div> </div>

gardieni

Gardă pentru cărucior gol

Această protecție împiedică utilizatorii să acceseze rutele legate de finalizarea comenzii și de facturare dacă coșul lor este gol. Acest lucru se datorează faptului că pentru a continua cu finalizarea comenzii, trebuie să existe o comandă validă. O comandă corespunde unui coș cu articole în el. Dacă există articole în coș, utilizatorul poate trece la o pagină păzită. Cu toate acestea, dacă coșul este gol, utilizatorul este redirecționat către o pagină cu coș goală.

 @Injectable({ providedIn: 'root' }) export class EmptyCartGuard implements CanActivate { constructor(private cart: CartService, private 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'); } }

Interceptori

Opțiuni Interceptor

Acest interceptor interceptează toate solicitările HTTP de ieșire și adaugă două opțiuni la cerere. Acestea sunt un antet Content-Type și o proprietate withCredentials . withCredentials specifică dacă o solicitare trebuie trimisă cu acreditări de ieșire, cum ar fi cookie-urile numai pentru http pe care le folosim. Folosim Content-Type pentru a indica faptul că trimitem resurse json către server.

 @Injectable() export class OptionsInterceptor implements HttpInterceptor { constructor() { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json'), withCredentials: true }); return next.handle(request); } }

Module de caracteristici

Această secțiune conține principalele caracteristici ale aplicației. După cum am menționat mai devreme, funcțiile sunt grupate în patru module: module de autentificare, produs, coș și finalizare.

Modulul Produse

Modulul de produse conține pagini care afișează produse la vânzare. Acestea includ pagina de produse și pagina cu lista de produse. Este structurat așa cum se arată mai jos.

 src/app/features/products ├── pages │ ├── product │ │ ├── product.component.css │ │ ├── product.component.html │ │ └── product.component.ts │ └── product-list │ ├── product-list.component.css │ ├── product-list.component.html │ └── product-list.component.ts └── products.module.ts

Pentru a-l genera și a componentelor sale:

 ng gm features/products ng gc features/products/pages/product ng gc features/products/pages/product-list

Acesta este fișierul modulului:

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

Componenta listei de produse

Această componentă afișează o listă paginată a produselor disponibile pentru vânzare. Este prima pagină care se încarcă atunci când pornește aplicația.

Produsele sunt afișate într-o grilă. Lista de materiale este cea mai bună componentă pentru aceasta. Pentru ca grila să răspundă, numărul de coloane al grilei se va modifica în funcție de dimensiunea ecranului. Serviciul BreakpointObserver ne permite să determinăm dimensiunea ecranului și să atribuim coloanele în timpul inițializării.

Pentru a obține produsele, numim metoda getProducts a SkuService . Returnează produsele dacă au succes și le atribuie grilei. Dacă nu, direcționăm utilizatorul către pagina de eroare.

Deoarece produsele afișate sunt paginate, vom avea o metodă getNextPage pentru a obține produsele suplimentare.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { cols = 4; length = 0; pageIndex = 0; pageSize = 20; pageSizeOptions: number[] = [5, 10, 20]; pageEvent!: PageEvent | void; products: Sku[] = []; constructor( private breakpointObserver: BreakpointObserver, private skus: SkuService, private router: Router, private header: HeaderService) { } ngOnInit() { this.getProducts(1, 20); this.header.setHeaderButtonsVisibility(true); this.breakpointObserver.observe([ Breakpoints.Handset, Breakpoints.Tablet, Breakpoints.Web ]).subscribe(result => { if (result.matches) { if (result.breakpoints['(max-width: 599.98px) and (orientation: portrait)'] || result.breakpoints['(max-width: 599.98px) and (orientation: landscape)']) { this.cols = 1; } else if (result.breakpoints['(min-width: 1280px) and (orientation: portrait)'] || result.breakpoints['(min-width: 1280px) and (orientation: landscape)']) { this.cols = 4; } else { this.cols = 3; } } }); } private getProducts(page: number, pageSize: number) { this.skus.getSkus(page, pageSize) .subscribe( skus => { this.products = skus; this.length = skus[0].__collectionMeta.recordCount; }, err => this.router.navigateByUrl('/error') ); } getNextPage(event: PageEvent) { this.getProducts(event.pageIndex + 1, event.pageSize); } trackSkus(index: number, item: Sku) { return `${item.id}-${index}`; } }

Șablonul este prezentat mai jos, iar stilul său poate fi găsit aici.

 <mat-grid-list cols="{{cols}}" rowHeight="400px" gutterSize="20px" class="grid-layout"> <mat-grid-tile *ngFor="let product of products; trackBy: trackSkus"> <mat-card> <img mat-card-image src="{{product.imageUrl}}" alt="product photo"> <mat-card-content> <mat-card-title matTooltip="{{product.name}}">{{product.name |wordWrap:35}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> </mat-card-content> <mat-card-actions> <button mat-flat-button color="primary" [routerLink]="['/product', product.id]"> View </button> </mat-card-actions> </mat-card> </mat-grid-tile> </mat-grid-list> <mat-paginator [length]="length" [pageIndex]="pageIndex" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" (page)="pageEvent = getNextPage($event)"> </mat-paginator>

Pagina va arăta așa.

Captură de ecran a paginii cu lista de produse
Captură de ecran a paginii cu listă de produse. (Previzualizare mare)

Componenta produsului

Odată ce un produs este selectat din pagina cu lista de produse, această componentă își afișează detaliile. Acestea includ numele complet, prețul și descrierea produsului. Există, de asemenea, un buton pentru a adăuga articolul în coșul de produse.

La inițializare, obținem id-ul produsului din parametrii rutei. Folosind id-ul, preluăm produsul din SkuService .

Când utilizatorul adaugă un articol în coș, este apelată metoda addItemToCart . În el, verificăm dacă a fost deja creată o comandă pentru coș. Dacă nu, se face unul nou utilizând OrderService . După care, un element rând este creat în ordinea care corespunde produsului. Dacă există deja o comandă pentru coș, este creat doar elementul rând. În funcție de starea solicitărilor, utilizatorului i se afișează un mesaj de tip snackbar.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product', templateUrl: './product.component.html', styleUrls: ['./product.component.css'] }) export class ProductComponent implements OnInit { id: string = ''; product!: Sku; quantity: number = 0; constructor( private route: ActivatedRoute, private skus: SkuService, private location: Location, private router: Router, private header: HeaderService, private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.paramMap .pipe( mergeMap(params => { const id = params.get('id') this.id = id ? id : ''; return this.skus.getSku(this.id); }), tap((sku) => { this.product = sku; }) ).subscribe({ error: (err) => this.router.navigateByUrl('/error') }); this.header.setHeaderButtonsVisibility(true); } addItemToCart() { if (this.quantity > 0) { if (this.cart.orderId == '') { this.orders.createOrder() .pipe( mergeMap((order: Order) => { this.cart.orderId = order.id || ''; return this.lineItems.createLineItem({ orderId: order.id, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }); }) ) .subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } else { this.lineItems.createLineItem({ orderId: this.cart.orderId, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }).subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } } else { this.snackBar.open('Select a quantity greater than 0.', 'Close', { duration: 8000 }); } } setQuantity(no: number) { this.quantity = no; } goBack() { this.location.back(); } private showSuccessSnackBar() { this.snackBar.open('Item successfully added to cart.', 'Close', { duration: 8000 }); } private showErrorSnackBar() { this.snackBar.open('Failed to add your item to the cart.', 'Close', { duration: 8000 }); } }

Șablonul ProductComponent este după cum urmează, iar stilul său este legat aici.

 <div> <mat-card *ngIf="product" class="product-card"> <img mat-card-image src="{{product.imageUrl}}" alt="Photo of a product"> <mat-card-content> <mat-card-title>{{product.name}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> <p> {{product.description}} </p> </mat-card-content> <mat-card-actions> <app-item-quantity [quantity]="quantity" [maxValue]="10" (setQuantityEvent)="setQuantity($event)"></app-item-quantity> <button mat-raised-button color="accent" (click)="addItemToCart()"> <mat-icon>add_shopping_cart</mat-icon> Add to cart </button> <button mat-raised-button color="primary" (click)="goBack()"> <mat-icon>storefront</mat-icon> Continue shopping </button> </mat-card-actions> </mat-card> </div>

Pagina va arăta așa.

Captură de ecran a paginii produsului
Captură de ecran a paginii produsului. (Previzualizare mare)

Modul de autentificare

Modulul Auth conține pagini responsabile pentru autentificare. Acestea includ paginile de conectare și înscriere. Este structurat după cum urmează.

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

Pentru a-l genera și a componentelor sale:

 ng gm features/auth ng gc features/auth/pages/signup ng gc features/auth/pages/login

Acesta este fișierul său de modul.

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

Componenta de înregistrare

Un utilizator se înscrie pentru un cont folosind această componentă. Pentru proces sunt necesare un prenume, un nume de familie, un e-mail și o parolă. De asemenea, utilizatorul trebuie să își confirme parola. Câmpurile de intrare vor fi create cu serviciul FormBuilder . Validarea este adăugată pentru a solicita ca toate intrările să aibă valori. Validarea suplimentară este adăugată câmpului de parolă pentru a asigura o lungime minimă de opt caractere. Un matchPasswords personalizat de potrivire a parolelor asigură că parola confirmată se potrivește cu parola inițială.

Când componenta este inițializată, butoanele de coș, de autentificare și de deconectare din antet sunt ascunse. Acest lucru este comunicat antetului folosind HeaderService .

După ce toate câmpurile sunt marcate ca valide, utilizatorul se poate înscrie. În metoda de signup , metoda createCustomer a CustomerService primește această intrare. Dacă înregistrarea are succes, utilizatorul este informat că contul său a fost creat cu succes folosind un snackbar. Acestea sunt apoi redirecționate către pagina de pornire.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-signup', templateUrl: './signup.component.html', styleUrls: ['./signup.component.css'] }) export class SignupComponent implements OnInit { signupForm = this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], confirmedPassword: ['', [Validators.required]] }, { validators: this.matchPasswords }); @ViewChild(FormGroupDirective) sufDirective: FormGroupDirective | undefined; constructor( private customer: CustomerService, private fb: FormBuilder, private snackBar: MatSnackBar, private router: Router, private header: HeaderService ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } matchPasswords(signupGroup: AbstractControl): ValidationErrors | null { const password = signupGroup.get('password')?.value; const confirmedPassword = signupGroup.get('confirmedPassword')?.value; return password == confirmedPassword ? null : { differentPasswords: true }; } get password() { return this.signupForm.get('password'); } get confirmedPassword() { return this.signupForm.get('confirmedPassword'); } signup() { const customer = this.signupForm.value; this.customer.createCustomer( customer.email, customer.password, customer.firstName, customer.lastName ).subscribe( () => { this.signupForm.reset(); this.sufDirective?.resetForm(); this.snackBar.open('Account successfully created. You will be redirected in 5 seconds.', 'Close', { duration: 5000 }); setTimeout(() => this.router.navigateByUrl('/'), 6000); }, err => this.snackBar.open('There was a problem creating your account.', 'Close', { duration: 5000 }) ); } }

Mai jos este șablonul pentru SignupComponent .

 <form [formGroup]="signupForm" (ngSubmit)="signup()"> <h1 class="mat-display-3">Create Account</h1> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput formControlName="email" type="email"> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput formControlName="password" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Confirm Password</mat-label> <input matInput formControlName="confirmedPassword" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <div *ngIf="confirmedPassword?.invalid && (confirmedPassword?.dirty || confirmedPassword?.touched)"> <mat-error *ngIf="signupForm.hasError('differentPasswords')"> Your passwords do not match. </mat-error> </div> <div *ngIf="password?.invalid && (password?.dirty || password?.touched)"> <mat-error *ngIf="password?.hasError('minlength')"> Your password should be at least 8 characters. </mat-error> </div> <button mat-flat-button color="primary" [disabled]="!signupForm.valid">Sign Up</button> </form>

Componenta se va dovedi după cum urmează.

Captură de ecran a paginii de înscriere
Captură de ecran a paginii de înscriere. (Previzualizare mare)

Componenta de autentificare

Un utilizator înregistrat se conectează la contul său cu această componentă. Trebuie introduse un e-mail și o parolă. Câmpurile lor de intrare corespunzătoare ar avea o validare care le face necesare.

Similar cu SignupComponent , butoanele coș, autentificare și deconectare din antet sunt ascunse. Vizibilitatea lor este setată folosind HeaderService în timpul inițializării componente.

Pentru a vă autentifica, acreditările sunt transmise AuthenticationService . Dacă reușește, starea de conectare a utilizatorului este setată folosind SessionService . Utilizatorul este apoi direcționat înapoi la pagina pe care se afla. Dacă nu reușește, este afișat un snackbar cu o eroare și câmpul pentru parolă este resetat.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { loginForm = this.fb.group({ email: ['', Validators.required], password: ['', Validators.required] }); constructor( private authService: AuthenticationService, private session: SessionService, private snackBar: MatSnackBar, private fb: FormBuilder, private header: HeaderService, private location: Location ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } login() { const credentials = this.loginForm.value; this.authService.login( credentials.email, credentials.password ).subscribe( () => { this.session.setLoggedInStatus(true); this.location.back(); }, err => { this.snackBar.open( 'Login failed. Check your login credentials.', 'Close', { duration: 6000 }); this.loginForm.patchValue({ password: '' }); } ); } }

Mai jos este șablonul LoginComponent .

 <form [formGroup]="loginForm" (ngSubmit)="login()"> <h1 class="mat-display-3">Login</h1> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput type="email" formControlName="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput type="password" formControlName="password" required> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="!loginForm.valid">Login</button> <p class="mat-h3">Not registered yet? <a routerLink="/signup">Create an account.</a></p> </form>

Iată o captură de ecran a paginii.

Captură de ecran a paginii de conectare
Captură de ecran a paginii de conectare. (Previzualizare mare)

Modul cărucior

Modulul coș conține toate paginile legate de coș. Acestea includ pagina cu rezumatul comenzii, o pagină cu coduri de cupon și card cadou și o pagină de coș goală. Este structurat după cum urmează.

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

Pentru a-l genera, rulați:

 ng gm features/cart ng gc features/cart/codes ng gc features/cart/empty ng gc features/cart/summary

Acesta este fișierul modulului.

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

Componenta coduri

După cum am menționat mai devreme, această componentă este utilizată pentru a adăuga orice cupon sau coduri de card cadou la o comandă. Acest lucru permite utilizatorului să aplice reduceri la totalul comenzii sale înainte de a trece la finalizarea comenzii.

Vor fi două câmpuri de introducere. Unul pentru cupoane și altul pentru codurile cardului cadou.

Codurile sunt adăugate prin actualizarea comenzii. Metoda updateOrder a OrderService actualizează comanda cu codurile. După care, ambele câmpuri sunt resetate și utilizatorul este informat despre succesul operațiunii cu un snackbar. Un snackbar este, de asemenea, afișat atunci când apare o eroare. Atât metodele addCoupon , cât și addGiftCard apelează metoda updateOrder .

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-codes', templateUrl: './codes.component.html', styleUrls: ['./codes.component.css'] }) export class CodesComponent { couponCode = new FormControl(''); giftCardCode = new FormControl(''); @ViewChild(FormControlDirective) codesDirective: FormControlDirective | undefined; constructor( private cart: CartService, private order: OrderService, private snackBar: MatSnackBar ) { } private updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) { this.order.updateOrder(order, params) .subscribe( () => { this.snackBar.open(`Successfully added ${codeType} code.`, 'Close', { duration: 8000 }); this.couponCode.reset(); this.giftCardCode.reset(); this.codesDirective?.reset(); }, err => this.snackBar.open(`There was a problem adding your ${codeType} code.`, 'Close', { duration: 8000 }) ); } addCoupon() { this.updateOrder({ id: this.cart.orderId, couponCode: this.couponCode.value }, [UpdateOrderParams.couponCode], 'coupon'); } addGiftCard() { this.updateOrder({ id: this.cart.orderId, giftCardCode: this.giftCardCode.value }, [UpdateOrderParams.giftCardCode], 'gift card'); } }

Șablonul este afișat mai jos, iar stilul său poate fi găsit la acest link.

 <div> <app-title title="Redeem a code" subtitle="Enter a coupon code or gift card" [centerText]="true"></app-title> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Coupon Code</mat-label> <input matInput [formControl]="couponCode" required> <mat-icon matPrefix>card_giftcard</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="couponCode.invalid" (click)="addCoupon()">Redeem</button> </div> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Gift Card Code</mat-label> <input matInput [formControl]="giftCardCode" required> <mat-icon matPrefix>redeem</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="giftCardCode.invalid" (click)="addGiftCard()">Redeem</button> </div> <button color="primary" mat-flat-button routerLink="/cart"> <mat-icon>shopping_cart</mat-icon> CONTINUE TO CART </button> </div>

Iată o captură de ecran a paginii.

Captură de ecran a paginii de coduri
Captură de ecran a paginii de coduri. (Previzualizare mare)

Componentă goală

Nu ar trebui să fie posibilă verificarea cu un coș gol. Trebuie să existe o gardă care să împiedice utilizatorii să acceseze paginile modulului de casă cu cărucioarele goale. Acest lucru a fost deja acoperit ca parte a CoreModule . Paznicul redirecționează cererile către paginile de plată cu un coș gol către EmptyCartComponent .

Este o componentă foarte simplă care are un text care indică utilizatorului că coșul său este gol. Are, de asemenea, un buton pe care utilizatorul poate face clic pentru a merge la pagina de pornire pentru a adăuga lucruri în coșul său. Deci vom folosi SimplePageComponent pentru a-l afișa. Iată șablonul.

 <app-simple-page title="Your cart is empty" subtitle="There is currently nothing in your cart. Head to the home page to add items." buttonText="GO TO HOME PAGE" icon="shopping_basket" [centerText]="true" route="/"> </app-simple-page>

Iată o captură de ecran a paginii.

Captură de ecran a paginii goale de coș
Captură de ecran a paginii goale de coș. (Previzualizare mare)

Componenta Rezumat

Această componentă rezumă coșul/comanda. Afișează toate articolele din coș, numele, cantitățile și imaginile acestora. În plus, acesta defalcă costul comenzii, inclusiv taxe, transport și reduceri. Utilizatorul ar trebui să poată vedea acest lucru și să decidă dacă este mulțumit de articole și costuri înainte de a trece la achiziție.

La inițializare, comanda și elementele rând ale acesteia sunt preluate folosind OrderService . Un utilizator ar trebui să poată modifica elementele rând sau chiar să le elimine din comandă. Elementele sunt eliminate atunci când este apelată metoda deleteLineItem . În ea, metoda deleteLineItem a LineItemService primește id-ul elementului rând de șters. Dacă ștergerea are succes, actualizăm numărul de articole din coș folosind CartService .

Utilizatorul este apoi direcționat către pagina clientului unde începe procesul de check-out. Metoda de checkout face rutare.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-summary', templateUrl: './summary.component.html', styleUrls: ['./summary.component.css'] }) export class SummaryComponent implements OnInit { order: Order = {}; summary: { name: string, amount: string | undefined, id: string }[] = []; constructor( private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar, private router: Router ) { } ngOnInit() { this.orders.getOrder(this.cart.orderId, GetOrderParams.cart) .subscribe( order => this.processOrder(order), err => this.showOrderError('retrieving your cart') ); } private processOrder(order: Order) { this.order = order; this.summary = [ { name: 'Subtotal', amount: order.formattedSubtotalAmount, id: 'subtotal' }, { name: 'Discount', amount: order.formattedDiscountAmount, id: 'discount' }, { name: 'Taxes (included)', amount: order.formattedTotalTaxAmount, id: 'taxes' }, { name: 'Shipping', amount: order.formattedShippingAmount, id: 'shipping' }, { name: 'Gift Card', amount: order.formattedGiftCardAmount, id: 'gift-card' } ]; } private showOrderError(msg: string) { this.snackBar.open(`There was a problem ${msg}.`, 'Close', { duration: 8000 }); } checkout() { this.router.navigateByUrl('/customer'); } 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(`Item successfully removed from cart.`, 'Close', { duration: 8000 }) }, err => this.showOrderError('deleting your order') ); } }

Mai jos este șablonul și stilul său este legat aici.

 <div class="container" *ngIf="order"> <h3>Order #{{order.number}} ({{order.skusCount}} items)</h3> <div class="line-item" *ngFor="let item of order.lineItems"> <div> <img *ngIf="item.imageUrl" class="image-xs" src="{{item.imageUrl}}" alt="product photo"> <div *ngIf="!item.imageUrl" class="image-xs no-image"></div> <div> <div>{{item.name}}</div> <div> {{item.formattedUnitAmount }} </div> </div> </div> <div> <app-item-quantity [quantity]="item.quantity || 0" [disabled]="true"></app-item-quantity> <div class="itemTotal"> {{item.formattedTotalAmount }} </div> <button mat-icon-button color="warn" (click)="deleteLineItem(item.id || '')"> <mat-icon>clear</mat-icon> </button> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem" *ngFor="let item of summary" [id]="item.id"> <h3 class="costLabel">{{item.name}}</h3> <p> {{item.amount }} </p> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem"> <h2>Total</h2> <h2> {{order.formattedTotalAmountWithTaxes}} </h2> </div> </div> <div> <button color="accent" mat-flat-button routerLink="/codes"> <mat-icon>redeem</mat-icon> ADD GIFT CARD/COUPON </button> <button color="primary" mat-flat-button (click)="checkout()"> <mat-icon>point_of_sale</mat-icon> CHECKOUT </button> </div> </div>

Iată o captură de ecran a paginii.

Captură de ecran a paginii rezumat
Captură de ecran a paginii rezumat. (Previzualizare mare)

Modulul Checkout

Acest modul este responsabil pentru procesul de checkout. Checkout implică furnizarea unei adrese de facturare și expediere, a unui e-mail pentru client și selectarea unei metode de livrare și plată. Ultimul pas al acestui proces este plasarea și confirmarea comenzii. Structura modulului este următoarea.

 src/app/features/checkout/ ├── components │ ├── address │ ├── address-list │ └── country-select └── pages ├── billing-address ├── cancel-payment ├── customer ├── payment ├── place-order ├── shipping-address └── shipping-methods

Acest modul este de departe cel mai mare și conține 3 componente și 7 pagini. Pentru a-l genera și componentele sale, rulați:

 ng gm features/checkout for comp in \ address address-list country-select; do \ ng gc "features/checkout/components/${comp}" \ ; done for page in \ billing-address cancel-payment customer \ payment place-order shipping-address \ shipping-methods; do \ ng gc "features/checkout/pages/${page}"; done

Acesta este fișierul modulului.

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

Componente

Componenta de selectare a țării

Această componentă permite utilizatorului să selecteze o țară ca parte a unei adrese. Componenta de selectare a materialului are un aspect destul de diferit în comparație cu câmpurile de introducere din formularul de adresă. Deci, de dragul uniformității, se folosește în schimb o componentă de meniu material.

Când componenta este inițializată, datele codului de țară sunt preluate folosind CountryService . Proprietatea countries deține valorile returnate de serviciu. Aceste valori vor fi adăugate în meniul din șablon.

Componenta are o proprietate de ieșire, setCountryEvent . Când este selectată o țară, acest eveniment emite codul alfa-2 al țării.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-country-select', templateUrl: './country-select.component.html', styleUrls: ['./country-select.component.css'] }) export class CountrySelectComponent implements OnInit { country: string = 'Country'; countries: Country[] = []; @Output() setCountryEvent = new EventEmitter<string>(); constructor(private countries: CountryService) { } ngOnInit() { this.countries.getCountries() .subscribe( countries => { this.countries = countries; } ); } setCountry(value: Country) { this.country = value.name || ''; this.setCountryEvent.emit(value.code); }}

Mai jos este șablonul său și aici este legat de stilul său.

 <button mat-stroked-button [matMenuTriggerFor]="countryMenu"> {{country}} <mat-icon>expand_more</mat-icon> </button> <mat-menu #countryMenu="matMenu"> <button *ngFor="let cnt of countries" (click)="setCountry(cnt)" mat-menu-item>{{cnt.name}}</button> </mat-menu>

Componenta Adresă

Acesta este un formular pentru captarea adreselor. Este folosit atât de paginile cu adresele de expediere, cât și de cele de facturare. O adresă validă Commerce Layer trebuie să conțină un nume și un prenume, o linie de adresă, un oraș, un cod poștal, un cod de stat, un cod de țară și un număr de telefon.

Serviciul FormBuilder va crea grupul de formulare. Deoarece această componentă este utilizată de mai multe pagini, are o serie de proprietăți de intrare și ieșire. Proprietățile de intrare includ textul butonului, titlul afișat și textul unei casete de selectare. Proprietățile de ieșire vor fi emițători de evenimente pentru când se face clic pe butonul pentru a crea adresa și altul pentru când valoarea casetei de selectare se schimbă.

Când se face clic pe butonul, se apelează metoda addAddress și evenimentul createAddress emite adresa completă. În mod similar, când caseta de selectare este bifată, evenimentul isCheckboxChecked emite valoarea casetei de selectare.

 @Component({ selector: 'app-address', templateUrl: './address.component.html', styleUrls: ['./address.component.css'] }) export class AddressComponent { @Input() buttonText: string = ''; @Input() showTitle?: boolean = false; @Output() createAddress = new EventEmitter<Address>(); @Input() checkboxText: string = ''; @Output() isCheckboxChecked = new EventEmitter<boolean>(); countryCode: string = ''; addressForm = this.fb.group({ firstName: [''], lastName: [''], line1: [''], city: [''], zipCode: [''], stateCode: [''], phone: [''] }); @ViewChild(FormGroupDirective) afDirective: FormGroupDirective | undefined; constructor(private fb: FormBuilder) { } setCountryCode(code: string) { this.countryCode = code; } addAddress() { this.createAddress.emit({ firstName: this.addressForm.get('firstName')?.value, lastName: this.addressForm.get('lastName')?.value, line1: this.addressForm.get('line1')?.value, city: this.addressForm.get('city')?.value, zipCode: this.addressForm.get('zipCode')?.value, stateCode: this.addressForm.get('stateCode')?.value || 'N/A', countryCode: this.countryCode, phone: this.addressForm.get('phone')?.value }); } setCheckboxValue(change: MatCheckboxChange) { if (this.isCheckboxChecked) { this.isCheckboxChecked.emit(change.checked); } } }

This is its template and its styling is linked here.

 <form [formGroup]="addressForm"> <p class="mat-headline" *ngIf="showTitle">Or add a new address</p> <div class="row"> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Address</mat-label> <input matInput formControlName="line1"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>City</mat-label> <input matInput formControlName="city"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>State Code</mat-label> <input matInput formControlName="stateCode"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Zip Code</mat-label> <input matInput formControlName="zipCode"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Phone</mat-label> <input matInput formControlName="phone"> </mat-form-field> <app-country-select (setCountryEvent)="setCountryCode($event)"></app-country-select> </div> <mat-checkbox color="accent" (change)="setCheckboxValue($event)"> {{checkboxText}} </mat-checkbox> <button mat-flat-button color="primary" (click)="addAddress()"> {{buttonText}} </button> </form>

Address List Component

When a customer logs in, they can access their existing addresses. Instead of having them re-enter an address, they can pick from an address list. This is the purpose of this component. On initialization, all the customer's addresses are fetched using the CustomerAddressService if they are logged in. We will check their login status using the SessionService .

This component has a setAddressEvent output property. When an address is selected, setAddressEvent emits its id to the parent component.

@Component({ selector: 'app-address-list', templateUrl: './address-list.component.html', styleUrls: ['./address-list.component.css'] }) export class AddressListComponent implements OnInit { addresses: CustomerAddress[] = []; @Output() setAddressEvent = new EventEmitter<string>(); constructor( private session: SessionService, private customerAddresses: CustomerAddressService, private snackBar: MatSnackBar ) { } ngOnInit() { this.session.loggedInStatus .pipe( mergeMap( status => iif(() => status, this.customerAddresses.getCustomerAddresses()) )) .subscribe( addresses => { if (addresses.length) { this.addresses = addresses } }, err => this.snackBar.open('There was a problem getting your existing addresses.', 'Close', { duration: 8000 }) ); } setAddress(change: MatRadioChange) { this.setAddressEvent.emit(change.value); } }

Iată șablonul său. Puteți găsi stilul său aici.

 <div> <p class="mat-headline">Pick an existing address</p> <mat-error *ngIf="!addresses.length">You have no existing addresses</mat-error> <mat-radio-group *ngIf="addresses.length" class="addresses" (change)="setAddress($event)"> <mat-card class="address" *ngFor="let address of addresses"> <mat-radio-button [value]="address.address?.id" color="primary"> <p>{{address.address?.firstName}} {{address.address?.lastName}},</p> <p>{{address.address?.line1}},</p> <p>{{address.address?.city}},</p> <p>{{address.address?.zipCode}},</p> <p>{{address.address?.stateCode}}, {{address.address?.countryCode}}</p> <p>{{address.address?.phone}}</p> </mat-radio-button> </mat-card> </mat-radio-group> </div>

Pagini

Componenta Clientului

O comandă trebuie să fie asociată cu o adresă de e-mail. Această componentă este un formular care captează adresa de e-mail a clientului. Când componenta este inițializată, adresa de e-mail a clientului actual este preluată dacă acesta este conectat. Primim clientul de la CustomerService . Dacă nu doresc să-și schimbe adresa de e-mail, acest e-mail va fi valoarea implicită.

Dacă e-mailul este schimbat sau un client nu este autentificat, comanda este actualizată cu e-mailul introdus. Utilizăm OrderService pentru a actualiza comanda cu noua adresă de e-mail. Dacă reușește, direcționăm clientul către pagina cu adresa de facturare.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-customer', templateUrl: './customer.component.html', styleUrls: ['./customer.component.css'] }) export class CustomerComponent implements OnInit { email = new FormControl('', [Validators.required, Validators.email]); constructor( private orders: OrderService, private customers: CustomerService, private cart: CartService, private router: Router, private snackBar: MatSnackBar ) { } ngOnInit() { this.customers.getCurrentCustomer() .subscribe( customer => this.email.setValue(customer.email) ); } addCustomerEmail() { this.orders.updateOrder( { id: this.cart.orderId, customerEmail: this.email.value }, [UpdateOrderParams.customerEmail]) .subscribe( () => this.router.navigateByUrl('/billing-address'), err => this.snackBar.open('There was a problem adding your email to the order.', 'Close', { duration: 8000 }) ); } }

Aici este șablonul componentei și aici este legat de stilul acestuia.

 <div> <app-title no="1" title="Customer" subtitle="Billing information and shipping address"></app-title> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput [formControl]="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="email.invalid" (click)="addCustomerEmail()"> PROCEED TO BILLING ADDRESS </button> </div>

Iată o captură de ecran a paginii clientului.

Captură de ecran a paginii clientului
Captură de ecran a paginii clientului. (Previzualizare mare)

Componenta Adresă de facturare

Componenta adresa de facturare permite unui client fie să adauge o nouă adresă de facturare, fie să aleagă dintre adresele existente. Utilizatorii care nu sunt autentificati trebuie să introducă o nouă adresă. Cei care s-au autentificat au opțiunea de a alege între adrese noi sau existente.

Proprietatea showAddress indică dacă adresele existente ar trebui să fie afișate pe componentă. sameShippingAddressAsBilling indică dacă adresa de expediere trebuie să fie aceeași cu adresa de facturare setată. Când un client selectează o adresă existentă, atunci id-ul acesteia este atribuit selectedCustomerAddressId .

Când componenta este inițializată, folosim SessionService pentru a verifica dacă utilizatorul curent este autentificat. Dacă este autentificat, vom afișa adresele existente dacă au.

După cum am menționat mai devreme, dacă un utilizator este conectat, acesta poate alege o adresă existentă ca adresă de facturare. În metoda updateBillingAddress , dacă sunt autentificați, adresa pe care o selectează este clonată și setată ca adresă de facturare a comenzii. Facem acest lucru prin actualizarea comenzii folosind metoda updateOrder a OrderService și furnizând adresa Id.

Dacă nu sunt autentificați, utilizatorul trebuie să furnizeze o adresă. Odată furnizată, adresa este creată folosind metoda createAddress . În el, AddressService preia intrarea și face noua adresă. După care, comanda este actualizată folosind id-ul adresei nou create. Dacă există o eroare sau oricare operațiune are succes, afișăm un snackbar.

Dacă aceeași adresă este selectată ca adresă de expediere, utilizatorul este direcționat către pagina de metode de expediere. Dacă doresc să furnizeze o adresă de expediere alternativă, vor fi direcționați către pagina cu adresa de expediere.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-billing-address', templateUrl: './billing-address.component.html', styleUrls: ['./billing-address.component.css'] }) export class BillingAddressComponent implements OnInit { showAddresses: boolean = false; sameShippingAddressAsBilling: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateBillingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameShippingAddressAsBilling(change: boolean) { this.sameShippingAddressAsBilling = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, billingAddressId: address.id }, [UpdateOrderParams.billingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, billingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.billingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameShippingAddressAsBilling, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.shippingAddressSameAsBilling]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private navigateTo(path: string) { setTimeout(() => this.router.navigateByUrl(path), 4000); } private showSuccessSnackBar() { this.snackBar.open('Billing address successfully added. Redirecting...', 'Close', { duration: 3000 }); if (this.sameShippingAddressAsBilling) { this.navigateTo('/shipping-methods'); } else { this.navigateTo('/shipping-address'); } } }

Iată șablonul. Acest link indică stilul său.

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

Așa va arăta pagina cu adresa de facturare.

Captură de ecran a paginii cu adresa de facturare
Captură de ecran a paginii cu adresa de facturare. (Previzualizare mare)

Componenta Adresa de livrare

Componenta adresă de expediere se comportă mult ca componenta adresă de facturare. Cu toate acestea, există câteva diferențe. În primul rând, textul afișat pe șablon este diferit. Celelalte diferențe cheie sunt în modul în care comanda este actualizată folosind OrderService odată ce o adresă este creată sau selectată. Câmpurile pe care le actualizează comanda sunt shippingAddressCloneId pentru adresele selectate și shippingAddress pentru adrese noi. Dacă un utilizator alege să schimbe adresa de facturare, pentru a fi aceeași cu adresa de expediere, câmpul billingAddressSameAsShipping este actualizat.

După ce o adresă de expediere este selectată și comanda este actualizată, utilizatorul este direcționat către pagina cu metodele de livrare.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-shipping-address', templateUrl: './shipping-address.component.html', styleUrls: ['./shipping-address.component.css'] }) export class ShippingAddressComponent implements OnInit { showAddresses: boolean = false; sameBillingAddressAsShipping: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateShippingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameBillingAddressAsShipping(change: boolean) { this.sameBillingAddressAsShipping = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, shippingAddressId: address.id }, [UpdateOrderParams.shippingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, shippingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.shippingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameBillingAddressAsShipping, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.billingAddressSameAsShipping]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private showSuccessSnackBar() { this.snackBar.open('Shipping address successfully added. Redirecting...', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/shipping-methods'), 4000); } }

Iată șablonul și stilul acestuia poate fi găsit aici.

 <app-title no="3" title="Shipping Address" subtitle="Address to ship package to"></app-title> <app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list> <mat-divider *ngIf="showAddresses"></mat-divider> <app-address [showTitle]="showAddresses" buttonText="PROCEED TO SHIPPING METHODS" checkboxText="Bill to the same address" (isCheckboxChecked)="setSameBillingAddressAsShipping($event)" (createAddress)="updateShippingAddress($event)"></app-address>

Pagina cu adresa de livrare va arăta astfel.

Captură de ecran a paginii cu adresa de expediere
Captură de ecran a paginii cu adresa de expediere. (Previzualizare mare)

Componenta Metode de expediere

Această componentă afișează numărul de expedieri necesare pentru ca o comandă să fie îndeplinită, metodele de expediere disponibile și costurile asociate acestora. Clientul poate selecta apoi metoda de expediere pe care o preferă pentru fiecare livrare.

Proprietatea shipments conține toate expedierile comenzii. shipmentsForm este formularul în care vor fi selectate metodele de expediere.

Când componenta este inițializată, comanda este preluată și va conține atât elementele rând, cât și expedierile. În același timp, obținem termenele de livrare pentru diferitele metode de expediere. Utilizăm OrderService pentru a primi comanda și DeliveryLeadTimeService pentru timpii de livrare. Odată ce ambele seturi de informații sunt returnate, acestea sunt combinate într-o serie de expedieri și atribuite proprietății shipments . Fiecare transport va conține articolele sale, metodele de expediere disponibile și costul corespunzător.

După ce utilizatorul a selectat o metodă de expediere pentru fiecare expediere, metoda de expediere selectată este actualizată pentru fiecare în setShipmentMethods . Dacă reușește, utilizatorul este direcționat către pagina de plăți.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-shipping-methods', templateUrl: './shipping-methods.component.html', styleUrls: ['./shipping-methods.component.css'] }) export class ShippingMethodsComponent implements OnInit { shipments: Shipment[] | undefined = []; shipmentsForm: FormGroup = this.fb.group({}); constructor( private orders: OrderService, private dlts: DeliveryLeadTimeService, private cart: CartService, private router: Router, private fb: FormBuilder, private shipments: ShipmentService, private snackBar: MatSnackBar ) { } ngOnInit() { combineLatest([ this.orders.getOrder(this.cart.orderId, GetOrderParams.shipments), this.dlts.getDeliveryLeadTimes() ]).subscribe( ([lineItems, deliveryLeadTimes]) => { let li: LineItem; let lt: DeliveryLeadTime[]; this.shipments = lineItems.shipments?.map((shipment) => { if (shipment.id) { this.shipmentsForm.addControl(shipment.id, new FormControl('', Validators.required)); } if (shipment.lineItems) { shipment.lineItems = shipment.lineItems.map(item => { li = this.findItem(lineItems, item.skuCode || ''); item.imageUrl = li.imageUrl; item.name = li.name; return item; }); } if (shipment.availableShippingMethods) { lt = this.findLocationLeadTime(deliveryLeadTimes, shipment); shipment.availableShippingMethods = shipment.availableShippingMethods?.map( method => { method.deliveryLeadTime = this.findMethodLeadTime(lt, method); return method; }); } return shipment; }); }, err => this.router.navigateByUrl('/error') ); } setShipmentMethods() { const shipmentsFormValue = this.shipmentsForm.value; combineLatest(Object.keys(shipmentsFormValue).map( key => this.shipments.updateShipment(key, shipmentsFormValue[key]) )).subscribe( () => { this.snackBar.open('Your shipments have been updated with a shipping method.', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/payment'), 4000); }, err => this.snackBar.open('There was a problem adding shipping methods to your shipments.', 'Close', { duration: 5000 }) ); } private findItem(lineItems: LineItem[], skuCode: string): LineItem { return lineItems.filter((item) => item.skuCode == skuCode)[0]; } private findLocationLeadTime(times: DeliveryLeadTime[], shipment: Shipment): DeliveryLeadTime[] { return times.filter((dlTime) => dlTime?.stockLocation?.id == shipment?.stockLocation?.id); } private findMethodLeadTime(times: DeliveryLeadTime[], method: ShippingMethod): DeliveryLeadTime { return times.filter((dlTime) => dlTime?.shippingMethod?.id == method?.id)[0]; } }

Iată șablonul și puteți găsi stilul la acest link.

 <form [formGroup]="shipmentsForm"> <app-title no="4" title="Shipping Methods" subtitle="How to ship your packages"></app-title> <div class="shipment-container" *ngFor="let shipment of shipments; let j = index; let isLast = last"> <h1>Shipment {{j+1}} of {{shipments?.length}}</h1> <div class="row" *ngFor="let item of shipment.lineItems"> <img class="image-xs" [src]="item.imageUrl" alt="product photo"> <div> <h4>{{item.name}}</h4> <p>{{item.skuCode}}</p> </div> <div> <p>Quantity: </p>{{item.quantity}} </div> </div> <mat-radio-group [formControlName]="shipment?.id || j"> <mat-radio-button *ngFor="let method of shipment.availableShippingMethods" [value]="method.id"> <div class="radio-button"> <p>{{method.name}}</p> <div> <p class="radio-label">Cost:</p> <p> {{method.formattedPriceAmount}}</p> </div> <div> <p class="radio-label">Timeline:</p> <p> Available in {{method.deliveryLeadTime?.minDays}}-{{method.deliveryLeadTime?.maxDays}} days</p> </div> </div> </mat-radio-button> </mat-radio-group> <mat-divider *ngIf="!isLast"></mat-divider> </div> <button mat-flat-button color="primary" [disabled]="shipmentsForm.invalid" (click)="setShipmentMethods()">PROCEED TO PAYMENT</button> </form>

Aceasta este o captură de ecran a paginii cu metode de expediere.

Captură de ecran a paginii cu metodele de expediere
Captură de ecran a paginii cu metodele de expediere. (Previzualizare mare)

Componenta Plăți

În această componentă, utilizatorul face clic pe butonul de plată dacă dorește să procedeze la plata comenzii cu Paypal. approvalUrl este linkul Paypal către care este direcționat utilizatorul când face clic pe butonul.

În timpul inițializării, primim comanda cu sursa de plată inclusă folosind OrderService . Dacă este setată o sursă de plată, obținem id-ul acesteia și recuperăm plata corespunzătoare Paypal de la PaypalPaymentService . Plata Paypal va conține adresa URL de aprobare. Dacă nu a fost setată nicio sursă de plată, actualizăm comanda cu Paypal ca metodă de plată preferată. Apoi procedăm la crearea unei noi plăți Paypal pentru comandă folosind PaypalPaymentService . De aici, putem obține adresa URL de aprobare de la comanda nou creată.

În cele din urmă, când utilizatorul face clic pe butonul, este redirecționat către Paypal unde poate aproba achiziția.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-payment', templateUrl: './payment.component.html', styleUrls: ['./payment.component.css'] }) export class PaymentComponent implements OnInit { approvalUrl: string = ''; constructor( private orders: OrderService, private cart: CartService, private router: Router, private payments: 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( (method) => method.paymentSourceType == 'paypal_payments' )[0]; return iif( () => paymentSourceId ? true : false, this.payments.getPaypalPayment(paymentSourceId || ''), this.orders.updateOrder({ id: orderId, paymentMethodId: paymentMethod?.id }, [UpdateOrderParams.paymentMethod]) .pipe(concatMap( order => this.payments.createPaypalPayment({ orderId: orderId, cancelUrl: `${environment.clientUrl}/cancel-payment`, returnUrl: `${environment.clientUrl}/place-order` }) )) ); })) .subscribe( paypalPayment => this.approvalUrl = paypalPayment?.approvalUrl || '', err => this.router.navigateByUrl('/error') ); } navigateToPaypal() { window.location.href = this.approvalUrl; } }

Iată șablonul său.

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

Iată cum va arăta pagina de plăți.

Captură de ecran a paginii de plată
Captură de ecran a paginii de plată. (Previzualizare mare)

Anulați Componenta de plată

Paypal necesită o pagină de anulare a plății. Această componentă servește acestui scop. Acesta este șablonul său.

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

Iată o captură de ecran a paginii.

Captură de ecran a paginii de anulare a plății
Captură de ecran a paginii de anulare a plății. (Previzualizare mare)

Componenta de plasare a comenzii

Acesta este ultimul pas în procesul de plată. Aici utilizatorul confirmă că într-adevăr dorește să plaseze comanda și să înceapă procesarea acesteia. Când utilizatorul aprobă plata Paypal, aceasta este pagina către care este redirecționat. Paypal adaugă un parametru de interogare privind ID-ul plătitorului la adresa URL. Acesta este ID-ul Paypal al utilizatorului.

Când componenta este inițializată, obținem parametrul de interogare payerId din url. Comanda este apoi preluată folosind OrderService cu sursa de plată inclusă. Id-ul sursei de plată inclusă este folosit pentru a actualiza plata Paypal cu id-ul plătitorului, folosind serviciul PaypalPayment . Dacă oricare dintre acestea eșuează, utilizatorul este redirecționat către pagina de eroare. Folosim proprietatea disableButton pentru a împiedica utilizatorul să plaseze comanda până când este setat ID-ul plătitorului.

Când fac clic pe butonul de plasare comandă, comanda este actualizată cu starea placed . După care coșul este golit, este afișat un snack bar de succes, iar utilizatorul este redirecționat către pagina de pornire.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-place-order', templateUrl: './place-order.component.html', styleUrls: ['./place-order.component.css'] }) export class PlaceOrderComponent implements OnInit { disableButton = true; constructor( private route: ActivatedRoute, private router: Router, private payments: PaypalPaymentService, private orders: OrderService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.queryParams .pipe( concatMap(params => { const payerId = params['PayerID']; const orderId = this.cart.orderId; return iif( () => payerId.length > 0, this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap(order => { const paymentSourceId = order.paymentSource?.id || ''; return iif( () => paymentSourceId ? paymentSourceId.length > 0 : false, this.payments.updatePaypalPayment(paymentSourceId, payerId) ); }) ) ); })) .subscribe( () => this.disableButton = false, () => this.router.navigateByUrl('/error') ); } placeOrder() { this.disableButton = true; this.orders.updateOrder({ id: this.cart.orderId, place: true }, [UpdateOrderParams.place]) .subscribe( () => { this.snackBar.open('Your order has been successfully placed.', 'Close', { duration: 3000 }); this.cart.clearCart(); setTimeout(() => this.router.navigateByUrl('/'), 4000); }, () => { this.snackBar.open('There was a problem placing your order.', 'Close', { duration: 8000 }); this.disableButton = false; } ); } }

Iată șablonul și stilul asociat.

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

Iată o captură de ecran a paginii.

Captură de ecran a paginii de plasare a comenzii
Captură de ecran a paginii de plasare a comenzii. (Previzualizare mare)

Modulul aplicației

Toate solicitările făcute către Commerce Layer, altele decât cele de autentificare, trebuie să conțină un simbol. Deci, în momentul inițializării aplicației, un token este preluat de pe /oauth/token de pe server și o sesiune este inițializată. Vom folosi simbolul APP_INITIALIZER pentru a oferi o funcție de inițializare în care simbolul este preluat. În plus, vom folosi simbolul HTTP_INTERCEPTORS pentru a furniza OptionsInterceptor pe care l-am creat mai devreme. Odată ce toate modulele sunt adăugate, fișierul modulului de aplicație ar trebui să arate cam așa.

 @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, BrowserAnimationsModule, AuthModule, ProductsModule, CartModule, CheckoutModule, CoreModule ], providers: [ { 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 { }

Componenta aplicației

Vom modifica șablonul componentei aplicației și stilul acestuia, pe care le puteți găsi aici.

 <div> <app-header></app-header> <div> <router-outlet></router-outlet> </div> </div>

Concluzie

În acest articol, am descris cum ați putea crea o aplicație Angular 11 de comerț electronic cu Commerce Layer și Paypal. Am atins, de asemenea, cum să structurați aplicația și cum puteți interfața cu un API de comerț electronic.

Deși această aplicație permite unui client să facă o comandă completă, nu este în niciun caz finalizată. Sunt atât de multe pe care le poți adăuga pentru a-l îmbunătăți. În primul rând, puteți alege să activați modificările cantității de articole în coș, să legați articolele din coș la paginile lor de produse, să optimizați componentele adresei, să adăugați protecții suplimentare pentru paginile de plată, cum ar fi pagina de plasare a comenzii și așa mai departe. Acesta este doar punctul de plecare.

Dacă doriți să înțelegeți mai multe despre procesul de realizare a unei comenzi de la început până la sfârșit, puteți consulta ghidurile Commerce Layer și API-ul. Puteți vizualiza codul pentru acest proiect în acest depozit.