Comment créer un site de commerce électronique avec Angular 11, Commerce Layer et Paypal

Publié: 2022-03-10
Résumé rapide ↬ Avoir une boutique e-commerce est crucial pour tout propriétaire de magasin car de plus en plus de clients se tournent vers les achats en ligne. Dans ce didacticiel, nous verrons comment créer un site de commerce électronique avec Angular 11. Le site utilisera Commerce Layer comme API de commerce électronique sans tête et utilisera Paypal pour traiter les paiements.

De nos jours, il est essentiel d'avoir une présence en ligne lors de la gestion d'une entreprise. Beaucoup plus d'achats se font en ligne que les années précédentes. Avoir un magasin de commerce électronique permet aux propriétaires de magasins d'ouvrir d'autres sources de revenus dont ils ne pourraient pas profiter avec un simple magasin physique. Cependant, d'autres propriétaires de magasins gèrent leur entreprise entièrement en ligne sans présence physique. Cela rend la boutique en ligne cruciale.

Des sites tels que Etsy, Shopify et Amazon facilitent la création d'un magasin assez rapidement sans avoir à se soucier du développement d'un site. Cependant, il peut y avoir des cas où les propriétaires de magasins peuvent souhaiter une expérience personnalisée ou peut-être économiser sur le coût de possession d'un magasin sur certaines de ces plateformes.

Les plates-formes d'API de commerce électronique sans tête fournissent des backends avec lesquels les sites de magasins peuvent s'interfacer. Ils gèrent tous les processus et données liés au magasin tels que les clients, les commandes, les expéditions, les paiements, etc. Tout ce dont vous avez besoin est une interface pour interagir avec ces informations. Cela donne aux propriétaires une grande flexibilité lorsqu'il s'agit de décider comment leurs clients expérimenteront leur boutique en ligne et comment ils choisiront de la gérer.

Dans cet article, nous expliquerons comment créer une boutique de commerce électronique à l'aide d'Angular 11. Nous utiliserons Commerce Layer comme API de commerce électronique sans tête. Bien qu'il puisse exister des tonnes de façons de traiter les paiements, nous vous montrerons comment n'en utiliser qu'une seule, Paypal.

  • Afficher le code source sur GitHub →

Conditions préalables

Avant de créer l'application, vous devez avoir installé Angular CLI. Nous l'utiliserons pour initialiser et échafauder l'application. Si vous ne l'avez pas encore installé, vous pouvez l'obtenir via npm.

 npm install -g @angular/cli

Vous aurez également besoin d'un compte de développeur Commerce Layer. À l'aide du compte développeur, vous devrez créer une organisation de test et l'ensemencer avec des données de test. L'ensemencement facilite le développement de l'application en premier lieu sans se soucier des données que vous devrez utiliser. Vous pouvez créer un compte sur ce lien et une organisation ici.

Tableau de bord des organisations de compte de développeur Commerce Layer
Tableau de bord des organisations de compte de développeur Commerce Layer où vous ajoutez votre organisation. ( Grand aperçu )
Formulaire de création d'organisations Commerce Layer
Cochez la case Seed with test data lors de la création d'une nouvelle organisation. ( Grand aperçu )

Enfin, vous aurez besoin d'un compte Paypal Sandbox. Avoir ce type de compte nous permettra de tester les transactions entre les entreprises et les utilisateurs sans risquer de l'argent réel. Vous pouvez en créer un ici. Un compte sandbox a une entreprise de test et un compte personnel de test déjà créés pour lui.

Plus après saut! Continuez à lire ci-dessous ↓

Couche commerciale et configuration Paypal

Pour rendre les paiements Paypal Sandbox possibles sur Commerce Layer, vous devez configurer des clés API. Rendez-vous sur l'aperçu des comptes de votre compte de développeur Paypal. Sélectionnez un compte professionnel et sous l'onglet Informations d'identification API des détails du compte, vous trouverez l' application par défaut sous REST Apps .

Onglet API Credentials dans la fenêtre contextuelle des détails du compte professionnel Paypal Sandbox
Où trouver l'application REST par défaut dans la fenêtre contextuelle des détails du compte professionnel Paypal. ( Grand aperçu )
Aperçu de l'application par défaut sur les paramètres du compte professionnel Paypal Sandbox
Présentation de l'application par défaut sur les paramètres du compte professionnel Paypal Sandbox où vous pouvez obtenir l'identifiant et le secret du client API REST. ( Grand aperçu )

Pour associer votre compte professionnel Paypal à votre organisation Commerce Layer, accédez au tableau de bord de votre organisation. Ici, vous allez ajouter une passerelle de paiement Paypal et un mode de paiement Paypal pour vos différents marchés. Sous Paramètres > Paiements , sélectionnez Passerelles de paiement > Paypal et ajoutez votre identifiant et votre secret Paypal.

Nouveau tableau de bord de passerelle de paiement sur Commerce Layer
Où sur le tableau de bord Commerce Layer pour créer une passerelle de paiement Paypal. ( Grand aperçu )

Après avoir créé la passerelle, vous devrez créer un mode de paiement Paypal pour chaque marché que vous ciblez afin de rendre Paypal disponible en option. Vous le ferez sous Paramètres > Paiements > Modes de paiement > Nouveau mode de paiement .

Tableau de bord des méthodes de paiement sur Commerce Layer
Où sur le tableau de bord Commerce Layer pour créer un mode de paiement Paypal. ( Grand aperçu )

Remarque sur les itinéraires utilisés

Commerce Layer fournit une route pour l'authentification et un autre ensemble de routes différent pour leur API. Leur route d'authentification /oauth/token échange des informations d'identification contre un jeton. Ce jeton est requis pour accéder à leur API. Le reste des routes d'API prend le modèle /api/:resource .

La portée de cet article ne couvre que la partie frontale de cette application. J'ai choisi de stocker les jetons côté serveur, d'utiliser des sessions pour suivre la propriété et de fournir des cookies http uniquement avec un identifiant de session au client. Cela ne sera pas traité ici car il sort du cadre de cet article. Cependant, les routes restent les mêmes et correspondent exactement à l'API Commerce Layer. Cependant, il existe quelques routes personnalisées non disponibles à partir de l'API Commerce Layer que nous utiliserons. Ceux-ci traitent principalement de la gestion des sessions. Je les soulignerai au fur et à mesure que nous y parviendrons et décrirai comment vous pouvez obtenir un résultat similaire.

Une autre incohérence que vous pouvez remarquer est que les corps de requête diffèrent de ce que l'API Commerce Layer requiert. Étant donné que les requêtes sont transmises à un autre serveur pour être remplies avec un jeton, j'ai structuré les corps différemment. C'était pour faciliter l'envoi des demandes. Chaque fois qu'il y a des incohérences dans les corps de requête, celles-ci seront signalées dans les services.

Comme cela est hors de portée, vous devrez décider comment stocker les jetons en toute sécurité. Vous devrez également modifier légèrement les corps de requête pour qu'ils correspondent exactement aux exigences de l'API Commerce Layer. En cas d'incohérence, je ferai un lien vers la référence de l'API et les guides détaillant comment structurer correctement le corps.

Structure de l'application

Pour organiser l'application, nous allons la décomposer en quatre parties principales. Une meilleure description de ce que fait chacun des modules est donnée dans les sections correspondantes :

  1. le module de base,
  2. le module de données,
  3. le module partagé,
  4. les modules de fonctionnalités.

Les modules de fonctionnalités regrouperont les pages et les composants associés. Il y aura quatre modules de fonctionnalités :

  1. le module d'authentification,
  2. le module produit,
  3. le module chariot,
  4. le module de paiement.

Au fur et à mesure que nous arrivons à chaque module, j'expliquerai quel est son objectif et décomposerai son contenu.

Vous trouverez ci-dessous une arborescence du dossier src/app et de l'emplacement de chaque module.

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

Générer l'application et ajouter des dépendances

Nous allons commencer par générer l'application. Notre organisation s'appellera The LIme Brand et disposera de données de test déjà ensemencées par Commerce Layer.

 ng new lime-app

Nous aurons besoin de quelques dépendances. Matériel principalement angulaire et jusqu'à destruction. Le matériau angulaire fournira les composants et le style. Jusqu'à ce que Destroy se désabonne automatiquement des observables lorsque les composants sont détruits. Pour les installer, lancez :

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

Actifs

Lors de l'ajout d'adresses à Commerce Layer, un code de pays alpha-2 doit être utilisé. Nous ajouterons un fichier json contenant ces codes au dossier assets dans assets/json/country-codes.json . Vous pouvez trouver ce fichier lié ici.

modes

Les composants que nous allons créer partagent un style global. Nous les placerons dans styles.css qui se trouve sur ce lien.

Environnement

Notre configuration sera composée de deux champs. L' apiUrl qui doit pointer vers l'API Commerce Layer. apiUrl est utilisé par les services que nous allons créer pour récupérer des données. Le clientUrl doit être le domaine sur lequel l'application s'exécute. Nous l'utilisons lors de la définition des URL de redirection pour Paypal. Vous pouvez trouver ce fichier sur ce lien.

Module partagé

Le module partagé contiendra des services, des canaux et des composants partagés entre les autres modules.

 ng gm shared

Il se compose de trois composants, un tuyau et deux services. Voici à quoi cela ressemblera.

 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

Nous utiliserons également le module partagé pour exporter certains composants Angular Material couramment utilisés. Cela facilite leur utilisation prête à l'emploi au lieu d'importer chaque composant dans différents modules. Voici ce que shared.module.ts contiendra.

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

Composants

Article Quantité Composant

Ce composant définit la quantité d'articles lors de leur ajout au panier. Il sera utilisé dans les modules panier et produits. Un sélecteur de matériaux aurait été un choix facile à cet effet. Cependant, le style de la sélection de matériaux ne correspondait pas aux entrées de matériaux utilisées dans tous les autres formulaires. Un menu matériel ressemblait beaucoup aux entrées matérielles utilisées. J'ai donc décidé de créer un composant de sélection avec lui à la place.

 ng gc shared/components/item-quantity

Le composant aura trois propriétés d'entrée et une propriété de sortie. quantity définit la quantité initiale d'éléments, maxValue indique le nombre maximum d'éléments pouvant être sélectionnés en une seule fois, et disabled indique si le composant doit être désactivé ou non. Le setQuantityEvent est déclenché lorsqu'une quantité est sélectionnée.

Lorsque le composant est initialisé, nous définirons les valeurs qui apparaissent dans le menu des matériaux. Il existe également une méthode appelée setQuantity qui émettra des événements setQuantityEvent .

Il s'agit du fichier composant.

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

C'est son modèle.

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

Voici son style.

 button { margin: 3px; }

Composant du titre

Ce composant se double d'un titre pas à pas ainsi que d'un titre simple sur certaines pages plus simples. Bien que Angular Material fournisse un composant pas à pas, ce n'était pas la meilleure solution pour un processus de paiement assez long, n'était pas aussi réactif sur les petits écrans et nécessitait beaucoup plus de temps pour sa mise en œuvre. Un titre plus simple pourrait cependant être réutilisé comme indicateur pas à pas et être utile sur plusieurs pages.

 ng gc shared/components/title

Le composant a quatre propriétés d'entrée : un title , un sous- subtitle , un nombre ( no ) et centerText , pour indiquer s'il faut centrer le texte du composant.

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

Ci-dessous son modèle. Vous pouvez trouver son style lié ici.

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

Composant de page simple

Il existe plusieurs cas où un titre, une icône et un bouton suffisaient pour une page. Il s'agit notamment d'une page 404, d'une page de panier vide, d'une page d'erreur, d'une page de paiement et d'une page de passation de commande. C'est à cela que servira le composant de page simple. Lorsque vous cliquez sur le bouton de la page, il redirige vers une route ou effectue une action en réponse à un buttonEvent .

Pour le faire :

 ng gc shared/components/simple-page

Il s'agit de son fichier composant.

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

Et son modèle contient :

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

Son style peut être trouvé ici.

Tuyaux

Tuyau d'enveloppement de mot

Certains noms de produits et autres types d'informations affichés sur le site sont très longs. Dans certains cas, il est difficile de faire en sorte que ces longues phrases soient enveloppées dans des composants matériels. Nous allons donc utiliser ce tuyau pour réduire les phrases à une longueur spécifiée et ajouter des ellipses à la fin du résultat.

Pour le créer, exécutez :

 ng g pipe shared/pipes/word-wrap

Il contiendra :

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

Prestations de service

Service de gestion des erreurs HTTP

Il existe un certain nombre de services http dans ce projet. La création d'un gestionnaire d'erreurs pour chaque méthode est répétitive. Il est donc logique de créer un gestionnaire unique pouvant être utilisé par toutes les méthodes. Le gestionnaire d'erreurs peut être utilisé pour formater une erreur et également transmettre les erreurs à d'autres plates-formes de journalisation externes.

Générez-le en exécutant :

 ng gs shared/services/http-error-handler

Ce service contiendra une seule méthode. La méthode formatera le message d'erreur à afficher selon qu'il s'agit d'une erreur client ou serveur. Cependant, il est encore possible de l'améliorer.

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

Service de stockage local

Nous utiliserons le stockage local pour suivre le nombre d'articles dans un panier. Il est également utile de stocker l'identifiant d'une commande ici. Une commande correspond à un panier sur Commerce Layer.

Pour générer le service de stockage local, exécutez :

 ng gs shared/services/local-storage

Le service contiendra quatre méthodes pour ajouter, supprimer et obtenir des éléments du stockage local et une autre pour l'effacer.

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

Module de données

Ce module est responsable de la récupération et de la gestion des données. C'est ce que nous utiliserons pour obtenir les données consommées par notre application. Ci-dessous sa structure :

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

Pour générer l'exécution du module :

 ng gm data

Des modèles

Les modèles définissent la manière dont les données que nous consommons à partir de l'API sont structurées. Nous aurons 16 déclarations d'interface. Pour les créer, exécutez :

 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

Le tableau suivant renvoie à chaque fichier et donne une description de chaque interface.

Interface La description
Adresse Représente une adresse générale.
Chariot Version côté client d'une commande suivant le nombre de produits qu'un client a l'intention d'acheter.
De campagne Code pays alpha-2.
Adresse du client Une adresse associée à un client.
Client Un utilisateur enregistré.
Délai de livraison Représente le temps qu'il faudra pour livrer une expédition.
Élément de campagne Un produit détaillé ajouté au panier.
Commande Un panier ou une collection d'éléments de ligne.
Mode de paiement Type de paiement mis à disposition pour une commande.
Source de paiement Un paiement associé à une commande.
Paiement PayPal Un paiement effectué via Paypal
Prix Prix ​​associé à un SKU.
Expédition Collection d'articles expédiés ensemble.
Mode de livraison Méthode par laquelle un colis est expédié.
UGS Une unité de stockage unique.
Emplacement des stocks Emplacement qui contient l'inventaire SKU.

Prestations de service

Ce dossier contient les services qui créent, récupèrent et manipulent les données d'application. Nous allons créer 11 services ici.

 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

Service d'adresse

Ce service crée et récupère des adresses. C'est important lors de la création et de l'attribution des adresses d'expédition et de facturation aux commandes. Il a deux méthodes. Un pour créer une adresse et un autre pour en récupérer une.

La route utilisée ici est /api/addresses . Si vous envisagez d'utiliser directement l'API Commerce Layer, assurez-vous de structurer les données comme illustré dans cet exemple.

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

Service de chariot

Le panier est responsable du maintien de la quantité d'articles ajoutés et de l'identifiant de la commande. Faire des appels d'API pour obtenir le nombre d'articles dans une commande à chaque fois qu'un nouvel article est créé peut être coûteux. Au lieu de cela, nous pourrions simplement utiliser le stockage local pour maintenir le compte sur le client. Cela élimine le besoin de récupérer des commandes inutiles chaque fois qu'un article est ajouté au panier.

Nous utilisons également ce service pour stocker l'identifiant de la commande. Un panier correspond à une commande sur Commerce Layer. Une fois le premier article ajouté au panier, une commande est créée. Nous devons conserver cet identifiant de commande afin de pouvoir le récupérer lors du processus de paiement.

De plus, nous avons besoin d'un moyen de communiquer à l'en-tête qu'un article a été ajouté au panier. L'en-tête contient le bouton du panier et affiche le nombre d'articles qu'il contient. Nous allons utiliser un observable d'un BehaviorSubject avec la valeur actuelle du panier. L'en-tête peut s'y abonner et suivre les modifications de la valeur du panier.

Enfin, une fois la commande terminée, la valeur du panier doit être effacée. Cela garantit qu'il n'y a pas de confusion lors de la création de nouvelles commandes ultérieures. Les valeurs enregistrées sont effacées une fois que la commande en cours est marquée comme passée.

Nous accomplirons tout cela en utilisant le service de stockage local créé précédemment.

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

Service Pays

Lors de l'ajout d'adresses sur Commerce Layer, le code de pays doit être un code alpha 2. Ce service lit un fichier json contenant ces codes pour chaque pays et le renvoie dans sa méthode 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'); } }

Service d'adresse client

Ce service est utilisé pour associer des adresses à des clients. Il récupère également une adresse spécifique ou toutes les adresses liées à un client. Il est utilisé lorsque le client ajoute ses adresses de livraison et de facturation à sa commande. La méthode createCustomer crée un client, getCustomerAddresses obtient toutes les adresses d'un client et getCustomerAddress obtient une spécifique.

Lors de la création d'une adresse client, veillez à structurer le corps du message conformément à cet exemple.

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

Service Clients

Les clients sont créés et leurs informations récupérées à l'aide de ce service. Lorsqu'un utilisateur s'inscrit, il devient un client et est créé à l'aide de createCustomerMethod . getCustomer renvoie le client associé à un identifiant spécifique. getCurrentCustomer renvoie le client actuellement connecté.

Lors de la création d'un client, structurez les données comme ceci. Vous pouvez ajouter leurs noms et prénoms aux métadonnées, comme indiqué dans ses attributs.

La route /api/customers/current n'est pas disponible sur Commerce Layer. Vous devrez donc déterminer comment obtenir le client actuellement connecté.

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

Service de délai de livraison

Ce service renvoie des informations sur les délais d'expédition à partir de divers emplacements de stockage.

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

Service d'éléments de campagne

Les articles ajoutés au panier sont gérés par ce service. Avec lui, vous pouvez créer un article au moment où il est ajouté au panier. Les informations d'un article peuvent également être récupérées. L'article peut également être mis à jour lorsque sa quantité change ou supprimé lorsqu'il est retiré du panier.

Lors de la création ou de la mise à jour d'éléments, structurez le corps de la requête comme indiqué dans cet exemple.

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

Service de commande

Semblable au service d'éléments de campagne, le service de commande vous permet de créer, mettre à jour, supprimer ou obtenir une commande. De plus, vous pouvez choisir d'obtenir séparément les envois associés à une commande à l'aide de la méthode getOrderShipments . Ce service est largement utilisé tout au long du processus de paiement.

Il existe différents types d'informations sur une commande qui sont requises lors du paiement. Comme il peut être coûteux de récupérer une commande entière et ses relations, nous spécifions ce que nous voulons obtenir d'une commande en utilisant GetOrderParams . L'équivalent de ceci sur l'API CL est le paramètre de requête include où vous répertoriez les relations de commande à inclure. Vous pouvez vérifier quels champs doivent être inclus pour le récapitulatif du panier ici et pour les différentes étapes de paiement ici.

De la même manière, lors de la mise à jour d'une commande, nous utilisons UpdateOrderParams pour spécifier les champs de mise à jour. En effet, sur le serveur qui remplit le jeton, certaines opérations supplémentaires sont effectuées en fonction du champ mis à jour. Cependant, si vous faites des demandes directes à l'API CL, vous n'avez pas besoin de le spécifier. Vous pouvez vous en passer car l'API CL ne vous oblige pas à les spécifier. Cependant, le corps de la requête doit ressembler à cet exemple.

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

Service de paiement Paypal

Ce service est responsable de la création et de la mise à jour des paiements Paypal pour les commandes. De plus, nous pouvons obtenir un paiement Paypal compte tenu de son identifiant. Le corps de la publication doit avoir une structure similaire à cet exemple lors de la création d'un paiement 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)); } }

Service d'expédition

Ce service reçoit un envoi ou le met à jour en fonction de son identifiant. Le corps de la demande d'une mise à jour d'expédition doit ressembler à cet exemple.

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

Service UGS

Le service SKU récupère les produits du magasin. Si plusieurs produits sont récupérés, ils peuvent être paginés et avoir une taille de page définie. La taille et le numéro de page doivent être définis en tant que paramètres de requête comme dans cet exemple si vous faites des demandes directes à l'API. Un seul produit peut également être récupéré en fonction de son identifiant.

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

Module de base

Le module principal contient tout ce qui est central et commun à l'ensemble de l'application. Ceux-ci incluent des composants tels que l'en-tête et des pages telles que la page 404. Les services responsables de l'authentification et de la gestion des sessions relèvent également de cette catégorie, ainsi que les intercepteurs et les gardes à l'échelle de l'application.

L'arborescence du module de base ressemblera à ceci.

 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

Pour générer le module et son contenu, exécutez :

 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

Le fichier du module principal devrait ressembler à ceci. Notez que les routes ont été enregistrées pour NotFoundComponent et 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 { }

Prestations de service

Le dossier services contient les services d'authentification, de session et d'en-tête.

Service d'authentification

Le service AuthenticationService vous permet d'acquérir des jetons client et client. Ces jetons sont utilisés pour accéder au reste des routes de l'API. Les jetons client sont renvoyés lorsqu'un utilisateur échange un e-mail et un mot de passe et dispose d'un plus large éventail d'autorisations. Les jetons client sont émis sans avoir besoin d'informations d'identification et ont des autorisations plus restreintes.

getClientSession obtient un jeton client. login obtient un jeton client. Les deux méthodes créent également une session. Le corps d'une demande de jeton client doit ressembler à ceci et celui d'un jeton client à ceci.

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

Composants

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. ( Grand aperçu )

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. ( Grand aperçu )

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.

Il existe une méthode de logout qui détruit la session d'un utilisateur et lui attribue un jeton client. Un jeton client est attribué car la session qui maintient son jeton client est détruite et un jeton est toujours requis pour chaque demande d'API. Un snack-bar matériel indique à l'utilisateur si sa session a été détruite avec succès ou non.

Nous utilisons le @UntilDestroy({ checkProperties: true }) pour indiquer que tous les abonnements doivent être automatiquement désabonnés à partir du moment où le composant est détruit.

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

Ci-dessous se trouve le modèle d'en-tête et lié ici est son style.

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

Gardes

Garde de chariot vide

Cette garde empêche les utilisateurs d'accéder aux itinéraires relatifs à la caisse et à la facturation si leur panier est vide. En effet, pour procéder au paiement, il doit y avoir une commande valide. Une commande correspond à un panier contenant des articles. S'il y a des articles dans le panier, l'utilisateur peut accéder à une page protégée. Cependant, si le panier est vide, l'utilisateur est redirigé vers une page de panier vide.

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

Intercepteurs

Intercepteur d'options

Cet intercepteur intercepte toutes les requêtes HTTP sortantes et ajoute deux options à la requête. Il s'agit d'un en-tête Content-Type et d'une propriété withCredentials . withCredentials spécifie si une demande doit être envoyée avec des informations d'identification sortantes telles que les cookies http uniquement que nous utilisons. Nous utilisons Content-Type pour indiquer que nous envoyons des ressources json au serveur.

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

Modules de fonctionnalités

Cette section contient les principales fonctionnalités de l'application. Comme mentionné précédemment, les fonctionnalités sont regroupées en quatre modules : modules d'authentification, de produit, de panier et de paiement.

Module Produits

Le module produits contient des pages qui affichent les produits en vente. Ceux-ci incluent la page du produit et la page de la liste des produits. Il est structuré comme indiqué ci-dessous.

 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

Pour le générer ainsi que ses composants :

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

C'est le fichier module :

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

Composant Liste de produits

Ce composant affiche une liste paginée des produits disponibles à la vente. C'est la première page qui est chargée au démarrage de l'application.

Les produits sont affichés dans une grille. La liste de la grille des matériaux est le meilleur composant pour cela. Pour rendre la grille réactive, le nombre de colonnes de la grille changera en fonction de la taille de l'écran. Le service BreakpointObserver nous permet de déterminer la taille de l'écran et d'affecter les colonnes lors de l'initialisation.

Pour obtenir les produits, nous appelons la méthode getProducts du SkuService . Il renvoie les produits en cas de succès et les affecte à la grille. Si ce n'est pas le cas, nous dirigeons l'utilisateur vers la page d'erreur.

Puisque les produits affichés sont paginés, nous aurons une méthode getNextPage pour obtenir les produits supplémentaires.

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

Le modèle est illustré ci-dessous et son style peut être trouvé ici.

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

La page ressemblera à ceci.

Capture d'écran de la page de liste de produits
Capture d'écran de la page de liste de produits. ( Grand aperçu )

Composant du produit

Une fois qu'un produit est sélectionné dans la page de liste de produits, ce composant affiche ses détails. Ceux-ci incluent le nom complet, le prix et la description du produit. Il y a aussi un bouton pour ajouter l'article au panier du produit.

Lors de l'initialisation, nous obtenons l'identifiant du produit à partir des paramètres de route. À l'aide de l'identifiant, nous récupérons le produit à partir du SkuService .

Lorsque l'utilisateur ajoute un article au panier, la méthode addItemToCart est appelée. Dans celui-ci, nous vérifions si une commande a déjà été créée pour le panier. Sinon, un nouveau est créé à l'aide de OrderService . Après quoi, un élément de ligne est créé dans l'ordre qui correspond au produit. Si une commande existe déjà pour le panier, seul l'élément de ligne est créé. En fonction de l'état des requêtes, un message snack est affiché à l'utilisateur.

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

Le modèle ProductComponent est le suivant et son style est lié ici.

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

La page ressemblera à ceci.

Capture d'écran de la page produit
Capture d'écran de la page du produit. ( Grand aperçu )

Module d'authentification

Le module Auth contient des pages responsables de l'authentification. Ceux-ci incluent les pages de connexion et d'inscription. Il est structuré comme suit.

 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

Pour le générer ainsi que ses composants :

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

Ceci est son fichier de module.

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

Composant d'inscription

Un utilisateur ouvre un compte à l'aide de ce composant. Un prénom, un nom de famille, un e-mail et un mot de passe sont requis pour le processus. L'utilisateur doit également confirmer son mot de passe. Les champs de saisie seront créés avec le service FormBuilder . La validation est ajoutée pour exiger que toutes les entrées aient des valeurs. Une validation supplémentaire est ajoutée au champ du mot de passe pour garantir une longueur minimale de huit caractères. Un validateur matchPasswords personnalisé garantit que le mot de passe confirmé correspond au mot de passe initial.

Lorsque le composant est initialisé, les boutons de panier, de connexion et de déconnexion dans l'en-tête sont masqués. Ceci est communiqué à l'en-tête à l'aide de HeaderService .

Une fois que tous les champs sont marqués comme valides, l'utilisateur peut alors s'inscrire. Dans la méthode d' signup , la méthode createCustomer de CustomerService reçoit cette entrée. Si l'inscription réussit, l'utilisateur est informé que son compte a été créé avec succès à l'aide d'un snack-bar. Ils sont ensuite redirigés vers la page d'accueil.

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

Vous trouverez ci-dessous le modèle de 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>

Le composant se révélera comme suit.

Capture d'écran de la page d'inscription
Capture d'écran de la page d'inscription. ( Grand aperçu )

Composant de connexion

Un utilisateur enregistré se connecte à son compte avec ce composant. Un email et un mot de passe doivent être saisis. Leurs champs de saisie correspondants auraient une validation qui les rend obligatoires.

Semblable au SignupComponent , les boutons de panier, de connexion et de déconnexion dans l'en-tête sont masqués. Leur visibilité est définie à l'aide de HeaderService lors de l'initialisation du composant.

Pour vous connecter, les informations d'identification sont transmises à AuthenticationService . En cas de succès, l'état de connexion de l'utilisateur est défini à l'aide de SessionService . L'utilisateur est ensuite redirigé vers la page sur laquelle il se trouvait. En cas d'échec, une barre d'en-cas s'affiche avec une erreur et le champ du mot de passe est réinitialisé.

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

Vous trouverez ci-dessous le modèle 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>

Voici une capture d'écran de la page.

Capture d'écran de la page de connexion
Capture d'écran de la page de connexion. ( Grand aperçu )

Module de panier

Le module panier contient toutes les pages liées au panier. Il s'agit notamment de la page de résumé de la commande, d'une page de code de coupon et de carte-cadeau et d'une page de panier vide. Il est structuré comme suit.

 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

Pour le générer, lancez :

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

C'est le fichier du module.

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

Composante des codes

Comme mentionné précédemment, ce composant est utilisé pour ajouter des codes de coupon ou de carte-cadeau à une commande. Cela permet à l'utilisateur d'appliquer des remises sur le total de sa commande avant de passer à la caisse.

Il y aura deux champs de saisie. Un pour les coupons et un autre pour les codes de cartes-cadeaux.

Les codes sont ajoutés en mettant à jour la commande. La méthode updateOrder de OrderService met à jour la commande avec les codes. Après quoi, les deux champs sont réinitialisés et l'utilisateur est informé du succès de l'opération avec un snack. Un snack-bar est également affiché lorsqu'une erreur se produit. Les méthodes addCoupon et addGiftCard appellent la méthode 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'); } }

Le modèle est illustré ci-dessous et son style peut être trouvé sur ce lien.

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

Voici une capture d'écran de la page.

Capture d'écran de la page des codes
Capture d'écran de la page des codes. ( Grand aperçu )

Composant vide

Il ne devrait pas être possible de payer avec un panier vide. Il doit y avoir un garde qui empêche les utilisateurs d'accéder aux pages du module de paiement avec des paniers vides. Cela a déjà été couvert dans le cadre du CoreModule . Le garde redirige les demandes vers les pages de paiement avec un panier vide vers le EmptyCartComponent .

C'est un composant très simple qui contient du texte indiquant à l'utilisateur que son panier est vide. Il comporte également un bouton sur lequel l'utilisateur peut cliquer pour accéder à la page d'accueil afin d'ajouter des éléments à son panier. Nous allons donc utiliser le SimplePageComponent pour l'afficher. Voici le modèle.

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

Voici une capture d'écran de la page.

Capture d'écran de la page du panier vide
Capture d'écran de la page du panier vide. ( Grand aperçu )

Composant récapitulatif

Ce composant récapitule le panier/la commande. Il répertorie tous les articles du panier, leurs noms, quantités et images. Il décompose en outre le coût de la commande, y compris les taxes, les frais d'expédition et les remises. L'utilisateur doit pouvoir le voir et décider s'il est satisfait des articles et du prix avant de passer à la caisse.

Lors de l'initialisation, la commande et ses éléments de ligne sont récupérés à l'aide de OrderService . Un utilisateur doit pouvoir modifier les éléments de campagne ou même les supprimer de la commande. Les éléments sont supprimés lorsque la méthode deleteLineItem est appelée. Dans celui-ci, la méthode deleteLineItem du LineItemService reçoit l'identifiant de l'élément de ligne à supprimer. Si une suppression réussit, nous mettons à jour le nombre d'articles dans le panier à l'aide de CartService .

L'utilisateur est ensuite redirigé vers la page client où il commence le processus de paiement. La méthode de checkout effectue le routage.

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

Vous trouverez ci-dessous le modèle et son style est lié ici.

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

Voici une capture d'écran de la page.

Capture d'écran de la page de résumé
Capture d'écran de la page de résumé. ( Grand aperçu )

Module de paiement

Ce module est responsable du processus de paiement. Le paiement implique de fournir une adresse de facturation et d'expédition, un e-mail client et de sélectionner un mode d'expédition et de paiement. La dernière étape de ce processus est le placement et la confirmation de la commande. La structure du module est la suivante.

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

Ce module est de loin le plus volumineux et contient 3 composants et 7 pages. Pour le générer et exécuter ses composants :

 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

C'est le fichier du module.

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

Composants

Composant de sélection de pays

Ce composant permet à un utilisateur de sélectionner un pays dans le cadre d'une adresse. Le composant de sélection de matériau a une apparence assez différente par rapport aux champs de saisie du formulaire d'adresse. Ainsi, par souci d'uniformité, un composant de menu matériel est utilisé à la place.

Lorsque le composant est initialisé, les données du code de pays sont extraites à l'aide de CountryService . La propriété countries contient les valeurs renvoyées par le service. Ces valeurs seront ajoutées au menu dans le modèle.

Le composant a une propriété de sortie, setCountryEvent . Lorsqu'un pays est sélectionné, cet événement émet le code alpha-2 du pays.

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

Ci-dessous se trouve son modèle et lié ici est son style.

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

Composant d'adresse

Il s'agit d'un formulaire de capture d'adresses. Il est utilisé à la fois par les pages d'adresse de livraison et de facturation. Une adresse Commerce Layer valide doit contenir un prénom et un nom, une ligne d'adresse, une ville, un code postal, un code d'état, un code de pays et un numéro de téléphone.

Le service FormBuilder créera le groupe de formulaires. Étant donné que ce composant est utilisé par plusieurs pages, il possède un certain nombre de propriétés d'entrée et de sortie. Les propriétés d'entrée incluent le texte du bouton, le titre affiché et le texte d'une case à cocher. Les propriétés de sortie seront des émetteurs d'événements lorsque le bouton est cliqué pour créer l'adresse et un autre lorsque la valeur de la case à cocher change.

Lorsque le bouton est cliqué, la méthode addAddress est appelée et l'événement createAddress émet l'adresse complète. De même, lorsque la case à cocher est cochée, l'événement isCheckboxChecked émet la valeur de la case à cocher.

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

Voici son gabarit. Vous pouvez trouver son style ici.

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

pages

Composant client

Une commande doit être associée à une adresse e-mail. Ce composant est un formulaire qui capture l'adresse e-mail du client. Lorsque le composant est initialisé, l'adresse e-mail du client actuel est récupérée s'il est connecté. Nous obtenons le client du CustomerService . S'il ne souhaite pas modifier son adresse e-mail, cet e-mail sera la valeur par défaut.

Si l'e-mail est modifié ou si un client n'est pas connecté, la commande est mise à jour avec l'e-mail saisi. Nous utilisons OrderService pour mettre à jour la commande avec la nouvelle adresse e-mail. En cas de succès, nous dirigeons le client vers la page d'adresse de facturation.

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

Voici le modèle de composant et lié voici son style.

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

Voici une capture d'écran de la page client.

Capture d'écran de la page client
Capture d'écran de la page client. ( Grand aperçu )

Composant d'adresse de facturation

Le composant d'adresse de facturation permet à un client d'ajouter une nouvelle adresse de facturation ou de choisir parmi ses adresses existantes. Les utilisateurs qui ne sont pas connectés doivent entrer une nouvelle adresse. Ceux qui se sont connectés ont la possibilité de choisir entre des adresses nouvelles ou existantes.

La propriété showAddress indique si les adresses existantes doivent être affichées sur le composant. sameShippingAddressAsBilling indique si l'adresse de livraison doit être la même que celle définie pour l'adresse de facturation. Lorsqu'un client sélectionne une adresse existante, son identifiant est attribué à selectedCustomerAddressId .

Lorsque le composant est initialisé, nous utilisons le SessionService pour vérifier si l'utilisateur actuel est connecté. S'il est connecté, nous afficherons ses adresses existantes s'il en a.

Comme mentionné précédemment, si un utilisateur est connecté, il peut choisir une adresse existante comme adresse de facturation. Dans la méthode updateBillingAddress , s'ils sont connectés, l'adresse qu'ils sélectionnent est clonée et définie comme adresse de facturation de la commande. Pour ce faire, nous mettons à jour la commande à l'aide de la méthode updateOrder de OrderService et fournissons l'identifiant d'adresse.

S'il n'est pas connecté, l'utilisateur doit fournir une adresse. Une fois fournie, l'adresse est créée à l'aide de la méthode createAddress . Dans celui-ci, l' AddressService prend l'entrée et crée la nouvelle adresse. Après quoi, la commande est mise à jour en utilisant l'identifiant de l'adresse nouvellement créée. S'il y a une erreur ou si l'une ou l'autre des opérations réussit, nous affichons un snack-bar.

Si la même adresse est sélectionnée comme adresse de livraison, l'utilisateur est redirigé vers la page des modes de livraison. S'ils souhaitent fournir une autre adresse de livraison, ils sont redirigés vers la page de l'adresse de livraison.

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

Voici le modèle. Ce lien pointe vers son style.

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

Voici à quoi ressemblera la page d'adresse de facturation.

Capture d'écran de la page d'adresse de facturation
Capture d'écran de la page d'adresse de facturation. ( Grand aperçu )

Composant de l'adresse de livraison

Le composant d'adresse d'expédition se comporte beaucoup comme le composant d'adresse de facturation. Cependant, il y a quelques différences. D'une part, le texte affiché sur le modèle est différent. Les autres principales différences résident dans la manière dont la commande est mise à jour à l'aide de OrderService une fois qu'une adresse est créée ou sélectionnée. Les champs que la commande met à jour sont shippingAddressCloneId pour les adresses sélectionnées et shippingAddress pour les nouvelles adresses. Si un utilisateur choisit de modifier l'adresse de facturation pour qu'elle soit identique à l'adresse de livraison, le champ billingAddressSameAsShipping est mis à jour.

Une fois qu'une adresse de livraison est sélectionnée et que la commande est mise à jour, l'utilisateur est redirigé vers la page des méthodes de livraison.

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

Voici le modèle et son style peut être trouvé ici.

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

La page d'adresse de livraison ressemblera à ceci.

Capture d'écran de la page d'adresse de livraison
Capture d'écran de la page d'adresse de livraison. ( Grand aperçu )

Composante des méthodes d'expédition

Ce composant affiche le nombre d'expéditions requises pour qu'une commande soit exécutée, les méthodes d'expédition disponibles et leurs coûts associés. Le client peut ensuite sélectionner une méthode d'expédition qu'il préfère pour chaque envoi.

La propriété des shipments contient toutes les expéditions de la commande. Le shipmentsForm est le formulaire dans lequel les sélections de méthode d'expédition seront effectuées.

Lorsque le composant est initialisé, la commande est récupérée et contiendra à la fois ses éléments de ligne et ses expéditions. En même temps, nous obtenons les délais de livraison pour les différents modes d'expédition. Nous utilisons OrderService pour obtenir la commande et DeliveryLeadTimeService pour les délais. Une fois que les deux ensembles d'informations sont renvoyés, ils sont combinés dans un tableau d'expéditions et affectés à la propriété des shipments . Chaque envoi contiendra ses articles, les méthodes d'expédition disponibles et le coût correspondant.

Une fois que l'utilisateur a sélectionné une méthode d'expédition pour chaque envoi, la méthode d'expédition sélectionnée est mise à jour pour chacun dans setShipmentMethods . En cas de succès, l'utilisateur est redirigé vers la page de paiement.

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

Voici le modèle et vous pouvez trouver le style sur ce lien.

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

Ceci est une capture d'écran de la page des méthodes d'expédition.

Capture d'écran de la page des méthodes d'expédition
Capture d'écran de la page des méthodes d'expédition. ( Grand aperçu )

Composante des paiements

Dans ce composant, l'utilisateur clique sur le bouton de paiement s'il souhaite procéder au paiement de sa commande avec Paypal. L' approvalUrl est le lien Paypal vers lequel l'utilisateur est dirigé lorsqu'il clique sur le bouton.

Lors de l'initialisation, nous obtenons la commande avec la source de paiement incluse à l'aide de OrderService . Si une source de paiement est définie, nous récupérons son identifiant et récupérons le paiement Paypal correspondant auprès de PaypalPaymentService . Le paiement Paypal contiendra l'url d'approbation. Si aucune source de paiement n'a été définie, nous mettons à jour la commande avec Paypal comme mode de paiement préféré. Nous procédons ensuite à la création d'un nouveau paiement Paypal pour la commande en utilisant le PaypalPaymentService . À partir de là, nous pouvons obtenir l'URL d'approbation de la commande nouvellement créée.

Enfin, lorsque l'utilisateur clique sur le bouton, il est redirigé vers Paypal où il peut approuver l'achat.

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

Voici son modèle.

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

Voici à quoi ressemblera la page des paiements.

Capture d'écran de la page de paiement
Capture d'écran de la page de paiement. ( Grand aperçu )

Annuler le composant de paiement

Paypal nécessite une page d'annulation de paiement. Ce composant sert à cela. C'est son modèle.

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

Voici une capture d'écran de la page.

Capture d'écran de la page d'annulation de paiement
Capture d'écran de la page d'annulation de paiement. ( Grand aperçu )

Placer un composant de commande

Il s'agit de la dernière étape du processus de paiement. Ici, l'utilisateur confirme qu'il souhaite effectivement passer la commande et commencer son traitement. Lorsque l'utilisateur approuve le paiement Paypal, c'est sur cette page qu'il est redirigé. Paypal ajoute un paramètre de requête d'identifiant de payeur à l'url. Il s'agit de l'identifiant Paypal de l'utilisateur.

Lorsque le composant est initialisé, nous obtenons le paramètre de requête payerId à partir de l'url. La commande est ensuite récupérée à l'aide de OrderService avec la source de paiement incluse. L'identifiant de la source de paiement incluse est utilisé pour mettre à jour le paiement Paypal avec l'identifiant du payeur, en utilisant le service PaypalPayment . Si l'un d'entre eux échoue, l'utilisateur est redirigé vers la page d'erreur. Nous utilisons la propriété disableButton pour empêcher l'utilisateur de passer la commande tant que l'identifiant du payeur n'est pas défini.

Lorsqu'ils cliquent sur le bouton de commande, la commande est mise à jour avec un statut placed . Après quoi le panier est effacé, un snack-bar réussi s'affiche et l'utilisateur est redirigé vers la page d'accueil.

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

Voici le modèle et son style associé.

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

Voici une capture d'écran de la page.

Capture d'écran de la page de passation de commande
Capture d'écran de la page de passation de commande. ( Grand aperçu )

Module d'application

Toutes les demandes adressées à Commerce Layer, autres que pour l'authentification, doivent contenir un jeton. Ainsi, au moment où l'application est initialisée, un jeton est récupéré à partir de la /oauth/token sur le serveur et une session est initialisée. Nous utiliserons le jeton APP_INITIALIZER pour fournir une fonction d'initialisation dans laquelle le jeton est récupéré. De plus, nous utiliserons le jeton HTTP_INTERCEPTORS pour fournir le OptionsInterceptor que nous avons créé précédemment. Une fois tous les modules ajoutés, le fichier du module d'application devrait ressembler à ceci.

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

Composant d'application

Nous allons modifier le modèle de composant d'application et son style que vous pouvez trouver ici.

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

Conclusion

Dans cet article, nous avons expliqué comment créer une application de commerce électronique Angular 11 avec Commerce Layer et Paypal. Nous avons également abordé la manière de structurer l'application et la manière dont vous pouvez vous interfacer avec une API de commerce électronique.

Bien que cette application permette à un client de passer une commande complète, celle-ci n'est en aucun cas terminée. Il y a tellement de choses que vous pourriez ajouter pour l'améliorer. D'une part, vous pouvez choisir d'activer les changements de quantité d'articles dans le panier, de lier les articles du panier à leurs pages de produits, d'optimiser les composants d'adresse, d'ajouter des gardes supplémentaires pour les pages de paiement comme la page de commande, etc. Ce n'est que le point de départ.

Si vous souhaitez en savoir plus sur le processus de passation d'une commande du début à la fin, vous pouvez consulter les guides et l'API Commerce Layer. Vous pouvez afficher le code de ce projet dans ce référentiel.