Jak zbudować witrynę e-commerce za pomocą Angular 11, warstwy handlowej i Paypal

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Posiadanie sklepu e-commerce jest kluczowe dla każdego właściciela sklepu, ponieważ coraz więcej klientów decyduje się na zakupy online. W tym samouczku omówimy, jak utworzyć witrynę e-commerce za pomocą Angular 11. Witryna użyje warstwy handlu jako bezgłowego interfejsu API handlu elektronicznego i użyje Paypal do przetwarzania płatności.

W dzisiejszych czasach obecność w Internecie jest niezbędna podczas prowadzenia firmy. Dużo więcej zakupów robi się online niż w poprzednich latach. Posiadanie sklepu e-commerce pozwala właścicielom sklepów otworzyć inne strumienie przychodów, z których nie mogliby skorzystać w zwykłym sklepie stacjonarnym. Inni właściciele sklepów prowadzą jednak swoje firmy online całkowicie bez fizycznej obecności. To sprawia, że ​​posiadanie sklepu internetowego jest kluczowe.

Witryny takie jak Etsy, Shopify i Amazon ułatwiają dość szybkie założenie sklepu bez martwienia się o rozwój witryny. Mogą jednak wystąpić sytuacje, w których właściciele sklepów mogą chcieć spersonalizowanych doświadczeń lub zaoszczędzić na kosztach posiadania sklepu na niektórych z tych platform.

Platformy API Headless e-commerce zapewniają backendy, z którymi mogą się komunikować witryny sklepów. Zarządzają wszystkimi procesami i danymi związanymi ze sklepem, takimi jak klient, zamówienia, przesyłki, płatności i tak dalej. Wszystko, co jest potrzebne, to interfejs do interakcji z tymi informacjami. Daje to właścicielom dużą elastyczność, jeśli chodzi o decydowanie o tym, jak klienci będą odbierali ich sklep internetowy i jak zdecydują się go prowadzić.

W tym artykule omówimy, jak zbudować sklep e-commerce za pomocą Angular 11. Użyjemy Commerce Layer jako naszego bezgłowego API e-commerce. Chociaż może istnieć mnóstwo sposobów przetwarzania płatności, pokażemy, jak używać tylko jednego, Paypal.

  • Wyświetl kod źródłowy na GitHub →

Warunki wstępne

Przed zbudowaniem aplikacji musisz mieć zainstalowany Angular CLI. Wykorzystamy go do inicjalizacji i szkieletowania aplikacji. Jeśli nie masz go jeszcze zainstalowanego, możesz go pobrać przez npm.

 npm install -g @angular/cli

Będziesz także potrzebować konta programisty Commerce Layer. Korzystając z konta programisty, musisz utworzyć organizację testową i umieścić w niej dane testowe. Seeding ułatwia rozwijanie aplikacji bez martwienia się o to, jakich danych będziesz musiał użyć. Możesz utworzyć konto pod tym linkiem, a organizację tutaj.

Pulpit nawigacyjny organizacji kont programistów Commerce Layer
Panel organizacji kont programistów Commerce Layer, w którym dodajesz swoją organizację. (duży podgląd)
Formularz tworzenia organizacji warstwy handlowej
Zaznacz pole Seed with test data podczas tworzenia nowej organizacji. (duży podgląd)

Na koniec będziesz potrzebować konta Paypal Sandbox. Posiadanie tego typu konta pozwoli nam testować transakcje między firmami a użytkownikami bez narażania rzeczywistych pieniędzy. Możesz go stworzyć tutaj. Konto piaskownicy ma już utworzone testowe konto biznesowe i testowe konto osobiste.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Warstwa handlowa i konfiguracja PayPal

Aby umożliwić płatności Paypal Sandbox w warstwie handlowej, musisz skonfigurować klucze API. Przejdź do przeglądu kont swojego konta programisty Paypal. Wybierz konto firmowe, a pod zakładką API poświadczeń szczegółów konta znajdziesz Domyślną aplikację pod REST Apps .

Karta API Credentials w wyskakującym okienku szczegółów konta biznesowego Paypal Sandbox
Gdzie znaleźć domyślną aplikację REST w wyskakującym okienku szczegółów konta biznesowego Paypal. (duży podgląd)
Przegląd domyślnej aplikacji w ustawieniach konta biznesowego Paypal Sandbox
Przegląd domyślnej aplikacji w ustawieniach konta biznesowego Paypal Sandbox, w którym można uzyskać identyfikator i klucz klienta REST API. (duży podgląd)

Aby powiązać swoje konto biznesowe Paypal z organizacją Commerce Layer, przejdź do pulpitu nawigacyjnego swojej organizacji. Tutaj dodasz bramkę płatności Paypal i metodę płatności Paypal dla różnych rynków. W sekcji Ustawienia > Płatności wybierz Bramki płatności > Paypal i dodaj swój identyfikator klienta Paypal oraz klucz tajny.

Nowy panel bramki płatności w warstwie handlowej
Gdzie na pulpicie Commerce Layer, aby utworzyć bramkę płatności Paypal. (duży podgląd)

Po utworzeniu bramki musisz utworzyć metodę płatności Paypal dla każdego rynku, na który kierujesz, aby udostępnić Paypal jako opcję. Zrobisz to w Ustawienia > Płatności > Metody płatności > Nowa metoda płatności .

Pulpit nawigacyjny metod płatności w warstwie handlowej
Gdzie w panelu Commerce Layer, aby utworzyć metodę płatności Paypal. (duży podgląd)

Uwaga na temat używanych tras

Warstwa handlowa udostępnia trasę do uwierzytelniania i inny inny zestaw tras dla ich interfejsu API. Trasa uwierzytelniania /oauth/token wymienia poświadczenia dla tokena. Ten token jest wymagany, aby uzyskać dostęp do ich interfejsu API. Pozostałe trasy API przyjmują wzorzec /api/:resource .

Zakres tego artykułu obejmuje tylko część frontendową tej aplikacji. Zdecydowałem się przechowywać tokeny po stronie serwera, używać sesji do śledzenia własności i dostarczać klientowi pliki cookie tylko http z identyfikatorem sesji. Nie zostanie to tutaj omówione, ponieważ wykracza to poza zakres tego artykułu. Jednak trasy pozostają takie same i dokładnie odpowiadają interfejsowi Commerce Layer API. Chociaż istnieje kilka niestandardowych tras niedostępnych w interfejsie Commerce Layer API, z których będziemy korzystać. Zajmują się one głównie zarządzaniem sesjami. Wskażę je, gdy do nich dojdziemy i opiszę, jak możesz osiągnąć podobny wynik.

Inną niespójnością, którą możesz zauważyć, jest to, że treść żądania różni się od tego, czego wymaga interfejs Commerce Layer API. Ponieważ żądania są przekazywane na inny serwer w celu wypełnienia tokenem, inaczej ułożyłem ciała. Miało to ułatwić wysyłanie próśb. W przypadku wystąpienia jakichkolwiek niespójności w organach wnioskujących zostaną one wskazane w usługach.

Ponieważ jest to poza zakresem, będziesz musiał zdecydować, jak bezpiecznie przechowywać tokeny. Musisz też nieznacznie zmodyfikować treść żądania, aby dokładnie odpowiadała wymaganiom interfejsu Commerce Layer API. W przypadku niespójności połączę się z referencją API i przewodnikami szczegółowo opisującymi, jak prawidłowo ustrukturyzować ciało.

Struktura aplikacji

Aby uporządkować aplikację, podzielimy ją na cztery główne części. Lepszy opis tego, co robi każdy z modułów, znajduje się w odpowiednich sekcjach:

  1. moduł podstawowy,
  2. moduł danych,
  3. udostępniony moduł,
  4. moduły funkcji.

Moduły funkcji będą grupować powiązane strony i komponenty razem. Będą cztery moduły fabularne:

  1. moduł uwierzytelniania,
  2. moduł produktu,
  3. moduł koszyka,
  4. moduł kasy.

Gdy dotrzemy do każdego modułu, wyjaśnię, jaki jest jego cel i przedstawię jego zawartość.

Poniżej znajduje się drzewo folderu src/app oraz lokalizacja każdego modułu.

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

Generowanie aplikacji i dodawanie zależności

Zaczniemy od wygenerowania aplikacji. Nasza organizacja będzie nazywać się The LIme Brand i będzie posiadała dane testowe już umieszczone w warstwie handlowej.

 ng new lime-app

Potrzebujemy kilku zależności. Głównie materiał kątowy i do zniszczenia. Angular Material zapewni komponenty i stylizację. Dopóki Destroy automatycznie anuluje subskrypcję obiektów do obserwacji, gdy komponenty zostaną zniszczone. Aby je zainstalować uruchom:

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

Aktywa

Podczas dodawania adresów do warstwy handlowej należy użyć kodu kraju alfa-2. Dodamy plik json zawierający te kody do folderu assets/json/country-codes.json assets Możesz znaleźć ten plik pod linkiem tutaj.

Style

Komponenty, które stworzymy, mają pewien globalny styl. Umieścimy je w styles.css , które można znaleźć pod tym linkiem.

Środowisko

Nasza konfiguracja będzie się składać z dwóch pól. apiUrl , który powinien wskazywać na interfejs Commerce Layer API. apiUrl jest używany przez usługi, które stworzymy do pobierania danych. clientUrl powinien być domeną, w której działa aplikacja. Używamy tego podczas ustawiania adresów URL przekierowania dla Paypal. Możesz znaleźć ten plik pod tym linkiem.

Wspólny moduł

Współdzielony moduł będzie zawierał usługi, potoki i komponenty współdzielone przez inne moduły.

 ng gm shared

Składa się z trzech komponentów, jednej rury i dwóch usług. Oto jak to będzie wyglądać.

 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

Będziemy również używać współdzielonego modułu do eksportu niektórych powszechnie używanych komponentów Angular Material. Ułatwia to korzystanie z nich po wyjęciu z pudełka, zamiast importowania każdego komponentu w różnych modułach. Oto, co będzie zawierał 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 { }

składniki

Pozycja Ilość Komponent

Ten składnik określa ilość towarów podczas dodawania ich do koszyka. Będzie używany w modułach koszyka i produktów. Wybór materiału byłby łatwym wyborem do tego celu. Jednak styl wybranego materiału nie odpowiadał materiałom wejściowym użytym we wszystkich innych formach. Menu materiałów wyglądało bardzo podobnie do użytych materiałów wejściowych. Postanowiłem więc stworzyć za jego pomocą wybrany komponent.

 ng gc shared/components/item-quantity

Komponent będzie miał trzy właściwości wejściowe i jedną właściwość wyjściową. quantity określa początkową ilość pozycji, maxValue wskazuje maksymalną ilość pozycji, jaką można wybrać za jednym razem, a disabled wskazuje, czy składnik powinien być wyłączony, czy nie. setQuantityEvent jest wyzwalane po wybraniu ilości.

Po zainicjowaniu komponentu ustawimy wartości, które pojawią się w menu materiałów. Istnieje również metoda o nazwie setQuantity , która będzie emitować zdarzenia setQuantityEvent .

To jest plik komponentu.

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

To jest jego szablon.

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

Oto jego stylizacja.

 button { margin: 3px; }

Składnik tytułu

Ten składnik może służyć zarówno jako tytuł krokowy, jak i zwykły tytuł na niektórych prostszych stronach. Chociaż Angular Material zapewnia komponent krokowy, nie nadawał się najlepiej do dość długiego procesu kasowania, nie był tak responsywny na mniejszych wyświetlaczach i wymagał znacznie więcej czasu na wdrożenie. Jednak prostszy tytuł może zostać zmieniony jako wskaźnik krokowy i być przydatny na wielu stronach.

 ng gc shared/components/title

Składnik ma cztery właściwości wejściowe: title , subtitle , number ( no ) i centerText , które wskazują, czy należy wyśrodkować tekst składnika.

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

Poniżej znajduje się jego szablon. Link do jego stylizacji znajdziesz tutaj.

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

Prosty składnik strony

Istnieje wiele przypadków, w których tytuł, ikona i przycisk były wszystkim, co było potrzebne na stronie. Należą do nich strona 404, pusta strona koszyka, strona błędu, strona płatności i strona składania zamówienia. W tym celu będzie służył prosty komponent strony. Kliknięcie przycisku na stronie spowoduje przekierowanie do trasy lub wykonanie jakiejś akcji w odpowiedzi na buttonEvent .

Zrobić to:

 ng gc shared/components/simple-page

To jest jego plik składowy.

 @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(); } } }

A jego szablon zawiera:

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

Jej stylizację znajdziesz tutaj.

Rury

Rura zawijania słów

Niektóre nazwy produktów i inne rodzaje informacji wyświetlane na stronie są naprawdę długie. W niektórych przypadkach uzyskanie tych długich zdań w celu zawinięcia elementów materialnych jest trudne. Więc użyjemy tej rury, aby skrócić zdania do określonej długości i dodać elipsy na końcu wyniku.

Aby go utworzyć, uruchom:

 ng g pipe shared/pipes/word-wrap

Będzie zawierał:

 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)}...`; } }

Usługi

Usługa obsługi błędów HTTP

W tym projekcie jest sporo usług http. Tworzenie obsługi błędów dla każdej metody jest powtarzalne. Dlatego stworzenie jednego programu obsługi, którego można używać wszystkimi metodami, ma sens. Program obsługi błędów może służyć do formatowania błędu, a także do przekazywania błędów do innych zewnętrznych platform rejestrowania.

Wygeneruj go, uruchamiając:

 ng gs shared/services/http-error-handler

Ta usługa będzie zawierać tylko jedną metodę. Metoda sformatuje wyświetlany komunikat o błędzie w zależności od tego, czy jest to błąd klienta, czy serwera. Jest jednak pole do dalszej poprawy.

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

Lokalna usługa przechowywania

Użyjemy lokalnego magazynu, aby śledzić liczbę produktów w koszyku. Przydatne jest również zapisanie tutaj identyfikatora zamówienia. Zamówienie odpowiada koszykowi w warstwie handlowej.

Aby wygenerować lokalną usługę magazynu, uruchom:

 ng gs shared/services/local-storage

Usługa będzie zawierać cztery metody dodawania, usuwania i pobierania elementów z pamięci lokalnej oraz inną do jej wyczyszczenia.

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

Moduł danych

Moduł ten odpowiada za pobieranie i zarządzanie danymi. Tego użyjemy, aby uzyskać dane, które wykorzystuje nasza aplikacja. Poniżej jego struktura:

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

Aby wygenerować uruchomienie modułu:

 ng gm data

Modele

Modele definiują strukturę danych, które wykorzystujemy z interfejsu API. Będziemy mieli 16 deklaracji interfejsu. Aby je utworzyć uruchom:

 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

Poniższa tabela zawiera linki do każdego pliku i zawiera opis każdego interfejsu.

Berło Opis
Adres Reprezentuje adres ogólny.
Wózek Wersja zamówienia po stronie klienta śledząca liczbę produktów, które klient zamierza kupić.
Kraj Kod kraju alfa-2.
Adres klienta Adres powiązany z klientem.
Klient Zarejestrowany użytkownik.
Czas realizacji dostawy Reprezentuje czas potrzebny na dostarczenie przesyłki.
Pozycja Poszczególny produkt dodany do koszyka.
Zamówienie Koszyk lub zbiór elementów zamówienia.
Metoda płatności Forma płatności udostępniona do zamówienia.
Źródło płatności Płatność powiązana z zamówieniem.
Płatność PayPal Płatność dokonana przez Paypal
Cena £ Cena powiązana z SKU.
Wysyłka Odbiór przedmiotów wysyłanych razem.
Sposób wysyłki Sposób wysyłki paczki.
SKU Wyjątkowa jednostka magazynowa.
Lokalizacja zapasów Lokalizacja, która zawiera magazyn SKU.

Usługi

Ten folder zawiera usługi, które tworzą, pobierają i manipulują danymi aplikacji. Stworzymy tutaj 11 usług.

 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

Usługa adresowa

Ta usługa tworzy i pobiera adresy. Jest to ważne podczas tworzenia i przypisywania adresów wysyłkowych i rozliczeniowych do zamówień. Ma dwie metody. Jeden, aby utworzyć adres, a drugi, aby go pobrać.

Użyta tutaj trasa to /api/addresses . Jeśli zamierzasz korzystać bezpośrednio z interfejsu Commerce Layer API, pamiętaj o uporządkowaniu danych, jak pokazano w tym przykładzie.

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

Usługa koszyka

Koszyk odpowiada za utrzymanie ilości dodanych pozycji oraz identyfikatora zamówienia. Wykonywanie wywołań interfejsu API w celu uzyskania liczby pozycji w zamówieniu za każdym razem, gdy tworzony jest nowy element zamówienia, może być kosztowne. Zamiast tego moglibyśmy po prostu użyć magazynu lokalnego, aby utrzymać licznik na kliencie. Eliminuje to konieczność dokonywania niepotrzebnych zamówień za każdym razem, gdy towar jest dodawany do koszyka.

Korzystamy również z tej usługi do przechowywania identyfikatora zamówienia. Koszyk odpowiada zamówieniu w warstwie handlowej. Po dodaniu pierwszego towaru do koszyka tworzone jest zamówienie. Musimy zachować ten identyfikator zamówienia, aby móc go pobrać podczas procesu realizacji transakcji.

Dodatkowo potrzebujemy sposobu na poinformowanie nagłówka, że ​​produkt został dodany do koszyka. Nagłówek zawiera przycisk koszyka i wyświetla w nim ilość przedmiotów. Użyjemy obserwowalnego obiektu BehaviorSubject z aktualną wartością koszyka. Nagłówek może subskrybować to i śledzić zmiany wartości koszyka.

Wreszcie, po zrealizowaniu zamówienia, wartość koszyka musi zostać wyczyszczona. Dzięki temu nie ma zamieszania przy tworzeniu kolejnych, nowszych zamówień. Zapisane wartości są usuwane po oznaczeniu bieżącego zamówienia jako złożone.

Wszystko to zrealizujemy za pomocą utworzonej wcześniej usługi przechowywania lokalnego.

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

Usługa krajowa

Podczas dodawania adresów w warstwie handlowej kod kraju musi być kodem alfa 2. Ta usługa odczytuje plik json zawierający te kody dla każdego kraju i zwraca go w swojej metodzie 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'); } }

Obsługa adresu klienta

Ta usługa służy do kojarzenia adresów z klientami. Pobiera również konkretny lub wszystkie adresy związane z klientem. Jest używany, gdy klient dodaje do zamówienia swoje adresy wysyłkowe i rozliczeniowe. Metoda createCustomer tworzy klienta, getCustomerAddresses pobiera wszystkie adresy klientów, a getCustomerAddress pobiera konkretny.

Tworząc adres klienta, upewnij się, że struktura treści posta jest zgodna z tym przykładem.

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

Obsługa klienta

Klienci są tworzeni, a ich informacje pobierane za pomocą tej usługi. Gdy użytkownik się zarejestruje, staje się klientem i jest tworzony za pomocą metody createCustomerMethod . getCustomer zwraca klienta powiązanego z określonym identyfikatorem. getCurrentCustomer zwraca aktualnie zalogowanego klienta.

Tworząc klienta, ustrukturyzuj dane w ten sposób. Możesz dodać ich imiona i nazwiska do metadanych, jak pokazano w ich atrybutach.

Trasa /api/customers/current nie jest dostępna w warstwie handlowej. Musisz więc dowiedzieć się, jak zdobyć aktualnie zalogowanego klienta.

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

Usługa czasu realizacji dostawy

Ta usługa zwraca informacje o terminach wysyłki z różnych lokalizacji magazynowych.

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

Usługa elementu zamówienia

Pozycje dodane do koszyka są zarządzane przez tę usługę. Dzięki niemu możesz stworzyć przedmiot w momencie dodania go do koszyka. Można również pobrać informacje o elemencie. Towar może być również aktualizowany w przypadku zmiany jego ilości lub usuwany po usunięciu z koszyka.

Tworząc elementy lub aktualizując je, ustrukturyzuj treść żądania, jak pokazano w tym przykładzie.

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

Zamów usługę

Podobnie jak usługa elementu zamówienia, usługa zamówień umożliwia tworzenie, aktualizowanie, usuwanie lub pobieranie zamówienia. Dodatkowo możesz wybrać opcję otrzymywania przesyłek powiązanych z zamówieniem osobno, korzystając z metody getOrderShipments . Ta usługa jest intensywnie używana podczas całego procesu realizacji transakcji.

Istnieją różne rodzaje informacji o zamówieniu, które są wymagane podczas realizacji transakcji. Ponieważ pobranie całego zamówienia i jego relacji może być kosztowne, określamy, co chcemy uzyskać z zamówienia za pomocą GetOrderParams . Odpowiednikiem tego w interfejsie CL API jest parametr zapytania include, w którym wymienia się relacje kolejności, które mają zostać uwzględnione. Tutaj możesz sprawdzić, jakie pola należy uwzględnić w podsumowaniu koszyka, a także na różnych etapach realizacji transakcji tutaj.

W ten sam sposób podczas aktualizacji zamówienia używamy UpdateOrderParams do określenia pól aktualizacji. Dzieje się tak, ponieważ na serwerze, który wypełnia token, wykonywane są dodatkowe operacje w zależności od aktualizowanego pola. Jeśli jednak wysyłasz bezpośrednie żądania do interfejsu API CL, nie musisz tego określać. Możesz się tego pozbyć, ponieważ interfejs CL API nie wymaga ich określania. Chociaż treść żądania powinna przypominać ten przykład.

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

Usługa płatności PayPal

Ta usługa jest odpowiedzialna za tworzenie i aktualizację płatności Paypal za zamówienia. Dodatkowo możemy otrzymać płatność Paypal na podstawie jej identyfikatora. Treść wiadomości powinna mieć strukturę podobną do tego przykładu podczas tworzenia płatności 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)); } }

Usługa wysyłki

Ta usługa pobiera przesyłkę lub aktualizuje ją na podstawie identyfikatora. Treść żądania aktualizacji przesyłki powinna wyglądać podobnie do tego przykładu.

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

Usługa SKU

Serwis SKU pobiera produkty ze sklepu. Jeśli pobieranych jest wiele produktów, można je podzielić na strony i ustawić rozmiar strony. Rozmiar strony i numer strony należy ustawić jako parametry zapytania, tak jak w tym przykładzie, jeśli wysyłasz bezpośrednie żądania do interfejsu API. Pojedynczy produkt można również pobrać na podstawie jego identyfikatora.

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

Moduł podstawowy

Moduł podstawowy zawiera wszystko, co jest kluczowe i wspólne dla całej aplikacji. Należą do nich komponenty, takie jak nagłówek i strony, takie jak strona 404. Tutaj również mieszczą się usługi odpowiedzialne za uwierzytelnianie i zarządzanie sesjami, a także przechwytywacze i strażnicy całej aplikacji.

Drzewo modułu podstawowego będzie wyglądało tak.

 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

Aby wygenerować moduł i jego zawartość uruchom:

 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

Plik modułu podstawowego powinien wyglądać tak. Należy zauważyć, że trasy zostały zarejestrowane dla 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 { }

Usługi

Folder services zawiera usługi uwierzytelniania, sesji i nagłówka.

Usługa uwierzytelniania

AuthenticationService umożliwia pozyskiwanie tokenów klienta i klienta. Te tokeny służą do uzyskiwania dostępu do pozostałych tras interfejsu API. Tokeny klienta są zwracane, gdy użytkownik wymienia dla niego adres e-mail i hasło i ma szerszy zakres uprawnień. Tokeny klienta są wydawane bez poświadczeń i mają węższe uprawnienia.

getClientSession pobiera token klienta. login otrzymuje token klienta. Obie metody tworzą również sesję. Treść żądania tokena klienta powinna wyglądać tak, a treść tokenu klienta w ten sposób.

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

składniki

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. (duży podgląd)

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. (duży podgląd)

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.

Istnieje metoda logout , która niszczy sesję użytkownika i przypisuje mu token klienta. Token klienta jest przypisywany, ponieważ sesja utrzymująca token klienta została zniszczona, a token jest nadal wymagany dla każdego żądania interfejsu API. Materialny pasek przekąskowy komunikuje użytkownikowi, czy jego sesja została pomyślnie zniszczona, czy nie.

Używamy @UntilDestroy({ checkProperties: true }) , aby wskazać, że wszystkie subskrypcje powinny być automatycznie anulowane od momentu zniszczenia komponentu.

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

Poniżej znajduje się szablon nagłówka, a link tutaj jest jego stylizacją.

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

Gwardia

Osłona pustego koszyka

Ta ochrona uniemożliwia użytkownikom dostęp do tras związanych z kasą i rozliczeniami, jeśli ich koszyk jest pusty. Dzieje się tak, ponieważ aby przejść do kasy, musi być ważne zamówienie. Zamówienie odpowiada koszykowi z produktami. Jeśli w koszyku znajdują się pozycje, użytkownik może przejść na stronę strzeżoną. Jeśli jednak koszyk jest pusty, użytkownik zostaje przekierowany na stronę z pustym koszykiem.

 @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'); } }

Przechwytywacze

Opcje Interceptor

Ten przechwytywacz przechwytuje wszystkie wychodzące żądania HTTP i dodaje do żądania dwie opcje. Są to nagłówek Content-Type i właściwość withCredentials . withCredentials określa, czy żądanie powinno zostać wysłane z wychodzącymi danymi uwierzytelniającymi, takimi jak używane przez nas pliki cookie tylko dla http. Używamy Content-Type , aby wskazać, że wysyłamy zasoby json do serwera.

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

Moduły funkcji

Ta sekcja zawiera główne funkcje aplikacji. Jak wspomniano wcześniej, funkcje są pogrupowane w cztery moduły: auth, product, cart i kasy.

Moduł produktów

Moduł produktów zawiera strony, które wyświetlają produkty na wyprzedaży. Należą do nich strona produktu i strona z listą produktów. Ma strukturę pokazaną poniżej.

 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

Aby go wygenerować i jego składniki:

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

To jest plik modułu:

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

Składnik listy produktów

Ten składnik wyświetla paginowaną listę dostępnych produktów do sprzedaży. Jest to pierwsza strona ładowana po uruchomieniu aplikacji.

Produkty są wyświetlane w siatce. Najlepszym do tego elementem jest lista siatki materiałów. Aby siatka była responsywna, liczba kolumn siatki będzie się zmieniać w zależności od rozmiaru ekranu. Usługa BreakpointObserver pozwala nam określić rozmiar ekranu i przypisać kolumny podczas inicjalizacji.

Aby uzyskać produkty, wywołujemy metodę getProducts usługi SkuService . Zwraca produkty, jeśli się powiedzie, i przypisuje je do siatki. Jeśli nie, kierujemy użytkownika na stronę błędu.

Ponieważ wyświetlane produkty są podzielone na strony, będziemy mieli metodę getNextPage , aby uzyskać dodatkowe produkty.

 @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}`; } }

Szablon pokazany jest poniżej, a jego stylizację znajdziesz tutaj.

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

Strona będzie wyglądać tak.

Zrzut ekranu strony z listą produktów
Zrzut ekranu strony z listą produktów. (duży podgląd)

Składnik produktu

Po wybraniu produktu ze strony z listą produktów ten składnik wyświetla jego szczegóły. Obejmują one pełną nazwę produktu, cenę i opis. Znajduje się tam również przycisk do dodania produktu do koszyka produktu.

Podczas inicjalizacji otrzymujemy id produktu z parametrów trasy. Używając identyfikatora, pobieramy produkt z SkuService .

Gdy użytkownik doda towar do koszyka, wywoływana jest metoda addItemToCart . W nim sprawdzamy, czy zostało już utworzone zamówienie na koszyk. Jeśli nie, nowy jest dokonywany za pomocą OrderService . Następnie tworzony jest element zamówienia w kolejności odpowiadającej produktowi. Jeśli zamówienie już istnieje dla koszyka, tworzona jest tylko pozycja. W zależności od statusu żądań, użytkownikowi wyświetlany jest komunikat na pasku przekąskowym.

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

Szablon ProductComponent jest następujący, a jego stylizacja jest tutaj połączona.

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

Strona będzie wyglądać tak.

Zrzut ekranu strony produktu
Zrzut ekranu strony produktu. (duży podgląd)

Moduł uwierzytelniania

Moduł Auth zawiera strony odpowiedzialne za uwierzytelnianie. Należą do nich strony logowania i rejestracji. Ma następującą strukturę.

 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

Aby go wygenerować i jego składniki:

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

To jest jego plik modułu.

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

Składnik rejestracji

Użytkownik rejestruje konto za pomocą tego komponentu. Do procesu wymagane jest imię, nazwisko, adres e-mail i hasło. Użytkownik musi również potwierdzić swoje hasło. Pola wejściowe zostaną utworzone za pomocą usługi FormBuilder . Walidacja jest dodawana, aby wymagać, aby wszystkie dane wejściowe miały wartości. Do pola hasła dodawana jest dodatkowa walidacja, aby zapewnić minimalną długość ośmiu znaków. Niestandardowy walidator matchPasswords zapewnia, że ​​potwierdzone hasło jest zgodne z hasłem początkowym.

Po zainicjowaniu komponentu przyciski koszyka, logowania i wylogowania w nagłówku są ukryte. Jest to przekazywane do nagłówka za pomocą HeaderService .

Po zaznaczeniu wszystkich pól jako prawidłowych, użytkownik może się zarejestrować. W metodzie signup dane wejściowe otrzymuje metoda createCustomer w CustomerService . Jeśli rejestracja się powiedzie, użytkownik zostanie poinformowany, że jego konto zostało pomyślnie utworzone za pomocą paska przekąskowego. Następnie są przekierowywane na stronę główną.

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

Poniżej znajduje się szablon dla 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>

Komponent będzie wyglądał następująco.

Zrzut ekranu strony rejestracji
Zrzut ekranu strony rejestracji. (duży podgląd)

Komponent logowania

Zarejestrowany użytkownik loguje się na swoje konto za pomocą tego komponentu. Należy wprowadzić adres e-mail i hasło. Odpowiadające im pola wejściowe miałyby walidację, która czyni je wymaganymi.

Podobnie jak w przypadku SignupComponent , przyciski koszyka, logowania i wylogowania w nagłówku są ukryte. Ich widoczność jest ustawiana przy użyciu HeaderService podczas inicjowania składnika.

Aby się zalogować, poświadczenia są przekazywane do AuthenticationService . Jeśli się powiedzie, stan logowania użytkownika jest ustawiany przy użyciu SessionService . Użytkownik jest następnie kierowany z powrotem do strony, na której był. W przypadku niepowodzenia wyświetlany jest pasek przekąskowy z błędem, a pole hasła jest resetowane.

 @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: '' }); } ); } }

Poniżej znajduje się szablon 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>

Oto zrzut ekranu strony.

Zrzut ekranu strony logowania
Zrzut ekranu strony logowania. (duży podgląd)

Moduł koszyka

Moduł koszyka zawiera wszystkie strony związane z koszykiem. Należą do nich strona podsumowania zamówienia, strona z kodem kuponu i karty podarunkowej oraz strona pustego koszyka. Ma następującą strukturę.

 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

Aby go wygenerować, uruchom:

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

To jest plik modułu.

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

Kody składnik

Jak wspomniano wcześniej, ten składnik służy do dodawania kodów kuponów lub kart podarunkowych do zamówienia. Dzięki temu użytkownik może zastosować rabaty do całości zamówienia przed przejściem do kasy.

Będą dwa pola wejściowe. Jeden na kupony, a drugi na kody kart podarunkowych.

Kody są dodawane poprzez aktualizację zamówienia. Metoda updateOrder usługi OrderService aktualizuje zamówienie o kody. Następnie oba pola są resetowane, a użytkownik jest informowany o powodzeniu operacji za pomocą paska przekąskowego. Pasek przekąskowy jest również wyświetlany, gdy wystąpi błąd. Obie metody addCoupon i addGiftCard wywołują metodę 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'); } }

Szablon pokazano poniżej, a jego stylizację można znaleźć pod tym linkiem.

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

Oto zrzut ekranu strony.

Zrzut ekranu strony kodów
Zrzut ekranu strony z kodami. (duży podgląd)

Pusty składnik

Nie powinno być możliwe wymeldowanie z pustym wózkiem. Musi istnieć strażnik, który uniemożliwi użytkownikom dostęp do stron modułu kasy z pustymi wózkami. Zostało to już omówione w ramach CoreModule . Strażnik przekierowuje żądania do stron kasy z pustym koszykiem do EmptyCartComponent .

Jest to bardzo prosty komponent, który zawiera tekst wskazujący użytkownikowi, że jego koszyk jest pusty. Posiada również przycisk, który użytkownik może kliknąć, aby przejść do strony głównej, aby dodać rzeczy do koszyka. Więc użyjemy SimplePageComponent , aby go wyświetlić. Oto szablon.

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

Oto zrzut ekranu strony.

Zrzut ekranu pustej strony koszyka
Zrzut ekranu pustej strony koszyka. (duży podgląd)

Komponent podsumowujący

Ten składnik podsumowuje koszyk/zamówienie. Zawiera listę wszystkich pozycji w koszyku, ich nazwy, ilości i zdjęcia. Dodatkowo rozkłada koszt zamówienia, w tym podatki, koszty wysyłki i rabaty. Użytkownik powinien móc to zobaczyć i zdecydować, czy jest zadowolony z pozycji i kosztów przed przejściem do kasy.

Podczas inicjowania zamówienie i jego pozycje są pobierane za pomocą OrderService . Użytkownik powinien mieć możliwość modyfikowania pozycji, a nawet usuwania ich z zamówienia. Elementy są usuwane po wywołaniu metody deleteLineItem . W nim metoda deleteLineItem usługi LineItemService otrzymuje identyfikator pozycji do usunięcia. Jeśli usunięcie się powiedzie, zaktualizujemy liczbę pozycji w koszyku za pomocą CartService .

Użytkownik jest następnie kierowany do strony klienta, gdzie rozpoczyna proces sprawdzania. Metoda checkout robi routing.

 @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') ); } }

Poniżej znajduje się szablon i link do jego stylizacji.

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

Oto zrzut ekranu strony.

Zrzut ekranu strony podsumowania
Zrzut ekranu strony podsumowania. (duży podgląd)

Moduł kasy

Ten moduł odpowiada za proces realizacji transakcji. Realizacja transakcji obejmuje podanie adresu rozliczeniowego i wysyłki, adresu e-mail klienta oraz wybranie metody wysyłki i płatności. Ostatnim krokiem tego procesu jest złożenie i potwierdzenie zamówienia. Struktura modułu jest następująca.

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

Ten moduł jest zdecydowanie największy i zawiera 3 komponenty i 7 stron. Aby go wygenerować i uruchomić jego komponenty:

 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

To jest plik modułu.

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

składniki

Kraj Wybierz składnik

Ten składnik pozwala użytkownikowi wybrać kraj jako część adresu. Komponent wyboru materiału ma zupełnie inny wygląd w porównaniu z polami wejściowymi w formularzu adresu. Dlatego w celu ujednolicenia zamiast tego używany jest składnik menu materialnego.

Podczas inicjowania komponentu dane kodu kraju są pobierane przy użyciu CountryService . Właściwość countries zawiera wartości zwrócone przez usługę. Te wartości zostaną dodane do menu w szablonie.

Komponent ma jedną właściwość wyjściową, setCountryEvent . Po wybraniu kraju to wydarzenie emituje kod alfa-2 kraju.

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

Poniżej znajduje się jego szablon, a link do jego stylizacji.

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

Komponent adresu

To jest formularz do przechwytywania adresów. Jest używany zarówno na stronie adresowej wysyłki, jak i rozliczeniowej. Prawidłowy adres warstwy handlowej powinien zawierać imię i nazwisko, wiersz adresu, miasto, kod pocztowy, kod stanu, kod kraju i numer telefonu.

Usługa FormBuilder utworzy grupę formularzy. Ponieważ ten składnik jest używany przez wiele stron, ma wiele właściwości wejściowych i wyjściowych. Właściwości wejściowe obejmują tekst przycisku, wyświetlany tytuł i tekst pola wyboru. Właściwości wyjściowe będą emiterami zdarzeń w przypadku kliknięcia przycisku w celu utworzenia adresu, a inne w przypadku zmiany wartości pola wyboru.

Po kliknięciu przycisku wywoływana jest metoda addAddress , a zdarzenie createAddress emituje pełny adres. Podobnie, gdy pole wyboru jest zaznaczone, zdarzenie isCheckboxChecked emituje wartość pola wyboru.

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

Oto jego szablon. Jego stylizację znajdziesz tutaj.

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

Strony

Komponent klienta

Zamówienie musi być powiązane z adresem e-mail. Ten składnik to formularz, który przechwytuje adres e-mail klienta. Po zainicjowaniu komponentu pobierany jest adres e-mail bieżącego klienta, jeśli jest on zalogowany. Pozyskujemy klienta z CustomerService . Jeśli nie chcą zmieniać swojego adresu e-mail, ten e-mail będzie wartością domyślną.

Jeśli adres e-mail zostanie zmieniony lub klient nie jest zalogowany, zamówienie zostanie zaktualizowane o wprowadzony adres e-mail. Korzystamy z OrderService , aby zaktualizować zamówienie z nowym adresem e-mail. Jeśli się powiedzie, przekierujemy klienta na stronę z adresem rozliczeniowym.

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

Oto szablon komponentu, a link tutaj jest jego stylem.

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

Oto zrzut ekranu strony klienta.

Zrzut ekranu strony klienta
Zrzut ekranu strony klienta. (duży podgląd)

Składnik adresu rozliczeniowego

Składnik adresu rozliczeniowego pozwala klientowi albo dodać nowy adres rozliczeniowy, albo wybrać z istniejących adresów. Użytkownicy niezalogowani muszą wprowadzić nowy adres. Osoby, które się zalogowały, mają możliwość wyboru między nowymi lub istniejącymi adresami.

Właściwość showAddress wskazuje, czy istniejące adresy powinny być pokazywane w komponencie. sameShippingAddressAsBilling wskazuje, czy adres wysyłki powinien być taki sam, jak ustawiony adres rozliczeniowy. Gdy klient wybierze istniejący adres, jego identyfikator jest przypisywany do selectedCustomerAddressId .

Po zainicjowaniu komponentu używamy SessionService do sprawdzenia, czy bieżący użytkownik jest zalogowany. Jeśli są zalogowani, wyświetlimy ich istniejące adresy, jeśli takie mają.

Jak wspomniano wcześniej, jeśli użytkownik jest zalogowany, jako adres rozliczeniowy może wybrać istniejący adres. W metodzie updateBillingAddress , jeśli są zalogowani, wybrany przez nich adres jest klonowany i ustawiany jako adres rozliczeniowy zamówienia. Robimy to poprzez aktualizację zamówienia za pomocą metody updateOrder usługi OrderService i podanie adresu Id.

Jeśli nie są zalogowani, użytkownik musi podać adres. Po podaniu adres jest tworzony przy użyciu metody createAddress . W nim AddressService pobiera dane wejściowe i tworzy nowy adres. Następnie zamówienie jest aktualizowane przy użyciu identyfikatora nowo utworzonego adresu. Jeśli wystąpi błąd lub którakolwiek operacja się powiedzie, pokazujemy pasek przekąskowy.

Jeśli ten sam adres zostanie wybrany jako adres wysyłki, użytkownik zostanie przekierowany do strony metod wysyłki. Jeśli chcą podać alternatywny adres wysyłki, są kierowani na stronę z adresem wysyłki.

 @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'); } } }

Oto szablon. Ten link wskazuje na jego stylizację.

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

Tak będzie wyglądać strona z adresem rozliczeniowym.

Zrzut ekranu strony z adresem rozliczeniowym
Zrzut ekranu strony z adresem rozliczeniowym. (duży podgląd)

Składnik adresu wysyłki

Składnik adresu wysyłki zachowuje się podobnie do składnika adresu rozliczeniowego. Istnieje jednak kilka różnic. Po pierwsze, tekst wyświetlany na szablonie jest inny. Inne kluczowe różnice dotyczą sposobu aktualizacji zamówienia za pomocą OrderService po utworzeniu lub wybraniu adresu. Pola, które aktualizuje zamówienie to shippingAddressCloneId dla wybranych adresów i shippingAddress dla nowych adresów. Jeśli użytkownik zdecyduje się zmienić adres rozliczeniowy, tak aby był taki sam jak adres wysyłkowy, pole billingAddressSameAsShipping jest aktualizowane.

Po wybraniu adresu wysyłki i zaktualizowaniu zamówienia, użytkownik zostaje przekierowany na stronę metod wysyłki.

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

Oto szablon i jego stylizację można znaleźć tutaj.

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

Strona z adresem wysyłki będzie wyglądać tak.

Zrzut ekranu strony z adresem wysyłki
Zrzut ekranu strony z adresem wysyłki. (duży podgląd)

Składnik metod wysyłki

Ten składnik wyświetla liczbę wysyłek wymaganych do realizacji zamówienia, dostępne metody wysyłki i związane z nimi koszty. Klient może następnie wybrać preferowaną przez siebie metodę wysyłki dla każdej przesyłki.

Właściwość shipments zawiera wszystkie przesyłki z zamówienia. Formularz shipmentsForm to formularz, w którym będą dokonywane wybory metody wysyłki.

Po zainicjowaniu składnika zamówienie jest pobierane i zawiera zarówno pozycje, jak i przesyłki. Jednocześnie otrzymujemy terminy realizacji dostaw dla różnych metod wysyłki. Korzystamy z OrderService , aby uzyskać zamówienie, a usługi DeliveryLeadTimeService w celu uzyskania terminów realizacji. Po zwróceniu obu zestawów informacji są one łączone w tablicę przesyłek i przypisywane do właściwości shipments . Każda przesyłka będzie zawierać swoje produkty, dostępne metody wysyłki oraz odpowiedni koszt.

Po wybraniu przez użytkownika metody wysyłki dla każdej przesyłki, wybrana metoda wysyłki jest aktualizowana dla każdej w setShipmentMethods . Jeśli się powiedzie, użytkownik zostanie przekierowany na stronę płatności.

 @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]; } }

Oto szablon, a stylizację znajdziesz pod tym linkiem.

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

To jest zrzut ekranu strony metod wysyłki.

Zrzut ekranu strony metod wysyłki
Zrzut ekranu strony metod wysyłki. (duży podgląd)

Składnik płatności

W tym komponencie użytkownik klika przycisk płatności, jeśli chce zapłacić za zamówienie za pomocą Paypal. approvalUrl to łącze Paypal, do którego użytkownik jest kierowany po kliknięciu przycisku.

Podczas inicjalizacji otrzymujemy zamówienie wraz ze źródłem płatności za pomocą OrderService . Jeśli źródło płatności jest ustawione, otrzymujemy jego identyfikator i pobieramy odpowiednią płatność Paypal z PaypalPaymentService . Płatność Paypal będzie zawierać adres URL zatwierdzenia. Jeśli nie określono źródła płatności, aktualizujemy zamówienie za pomocą Paypal jako preferowanej metody płatności. Następnie przystępujemy do tworzenia nowej płatności Paypal za zamówienie za pomocą PaypalPaymentService . Stąd możemy uzyskać adres URL zatwierdzenia z nowo utworzonego zamówienia.

Wreszcie, gdy użytkownik kliknie przycisk, zostanie przekierowany do Paypal, gdzie może zatwierdzić zakup.

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

Oto jego szablon.

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

Oto jak będzie wyglądać strona płatności.

Zrzut ekranu strony płatności
Zrzut ekranu strony płatności. (duży podgląd)

Anuluj składnik płatności

PayPal wymaga strony anulowania płatności. Ten składnik służy temu celowi. To jest jego szablon.

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

Oto zrzut ekranu strony.

Zrzut ekranu strony anulowania płatności
Zrzut ekranu strony anulowania płatności. (duży podgląd)

Komponent złóż zamówienie

To ostatni krok w procesie realizacji transakcji. Tutaj użytkownik potwierdza, że ​​rzeczywiście chce złożyć zamówienie i rozpocząć jego realizację. Kiedy użytkownik zatwierdzi płatność Paypal, jest to strona, na którą zostaje przekierowany. Paypal dodaje parametr zapytania identyfikatora płatnika do adresu URL. To jest identyfikator Paypal użytkownika.

Po zainicjowaniu komponentu otrzymujemy parametr zapytania payerId z adresu URL. Zamówienie jest następnie pobierane za pomocą OrderService z dołączonym źródłem płatności. Identyfikator zawartego źródła płatności służy do aktualizacji płatności Paypal o identyfikator płatnika, za pomocą usługi PaypalPayment . Jeśli którakolwiek z tych czynności nie powiedzie się, użytkownik zostanie przekierowany na stronę błędu. Używamy właściwości disableButton , aby uniemożliwić użytkownikowi złożenie zamówienia, dopóki nie zostanie ustawiony identyfikator płatnika.

Gdy klikną przycisk złóż zamówienie, zamówienie zostanie zaktualizowane ze statusem placed . Następnie koszyk zostaje wyczyszczony, wyświetlany jest udany pasek z przekąskami, a użytkownik zostaje przekierowany na stronę główną.

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

Oto szablon i związana z nim stylizacja.

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

Oto zrzut ekranu strony.

Zrzut ekranu strony składania zamówienia
Zrzut ekranu strony składania zamówienia. (duży podgląd)

Moduł aplikacji

Wszystkie żądania kierowane do Warstwy Handlowej, inne niż żądania uwierzytelnienia, muszą zawierać token. Tak więc w momencie zainicjowania aplikacji token jest pobierany z trasy /oauth/token na serwerze i inicjowana jest sesja. Użyjemy tokenu APP_INITIALIZER , aby zapewnić funkcję inicjującą, w której token jest pobierany. Dodatkowo użyjemy tokenu HTTP_INTERCEPTORS , aby udostępnić utworzony wcześniej OptionsInterceptor . Po dodaniu wszystkich modułów plik modułu aplikacji powinien wyglądać mniej więcej tak.

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

Komponent aplikacji

Zmodyfikujemy szablon komponentu aplikacji i jego stylizację, które znajdziesz tutaj.

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

Wniosek

W tym artykule omówiliśmy, w jaki sposób można stworzyć aplikację e-commerce Angular 11 za pomocą Commerce Layer i Paypal. Dotknęliśmy również struktury aplikacji i sposobu, w jaki można łączyć się z interfejsem API handlu elektronicznego.

Chociaż ta aplikacja pozwala klientowi na złożenie kompletnego zamówienia, nie jest w żaden sposób skończona. Jest tyle rzeczy, które można by dodać, aby to ulepszyć. Po pierwsze, możesz włączyć zmiany ilości pozycji w koszyku, połączyć pozycje koszyka z ich stronami produktów, zoptymalizować komponenty adresu, dodać dodatkowe osłony dla stron kasy, takich jak strona składania zamówień i tak dalej. To dopiero początek.

Jeśli chcesz dowiedzieć się więcej o procesie składania zamówienia od początku do końca, możesz zapoznać się z przewodnikami po warstwie handlowej i interfejsem API. Możesz wyświetlić kod tego projektu w tym repozytorium.