Comment créer un site de commerce électronique avec Angular 11, Commerce Layer et Paypal
Publié: 2022-03-10De 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.
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.
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 .
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.
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 .
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 :
- le module de base,
- le module de données,
- le module partagé,
- 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 :
- le module d'authentification,
- le module produit,
- le module chariot,
- 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.
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.