Come costruire un sito di e-commerce con Angular 11, Commerce Layer e Paypal
Pubblicato: 2022-03-10Al giorno d'oggi è essenziale avere una presenza online quando si gestisce un'impresa. Si effettuano molti più acquisti online rispetto agli anni precedenti. Avere un negozio di e-commerce consente ai proprietari di negozi di aprire altri flussi di entrate che non potrebbero sfruttare con un semplice negozio fisico. Altri proprietari di negozi, tuttavia, gestiscono le loro attività online interamente senza una presenza fisica. Questo rende fondamentale avere un negozio online.
Siti come Etsy, Shopify e Amazon semplificano la creazione di un negozio abbastanza rapidamente senza doversi preoccupare di sviluppare un sito. Tuttavia, potrebbero esserci casi in cui i proprietari di negozi potrebbero desiderare un'esperienza personalizzata o magari risparmiare sul costo di possedere un negozio su alcune di queste piattaforme.
Le piattaforme API di e-commerce headless forniscono backend con cui i siti dei negozi possono interfacciarsi. Gestiscono tutti i processi e i dati relativi al negozio come cliente, ordini, spedizioni, pagamenti e così via. Tutto ciò che serve è un frontend per interagire con queste informazioni. Ciò offre ai proprietari molta flessibilità quando si tratta di decidere come i loro clienti sperimenteranno il loro negozio online e come scelgono di gestirlo.
In questo articolo, tratteremo come costruire un negozio di e-commerce utilizzando Angular 11. Useremo Commerce Layer come nostra API di e-commerce senza testa. Sebbene possano esserci molti modi per elaborare i pagamenti, dimostreremo come utilizzarne solo uno, Paypal.
- Visualizza il codice sorgente su GitHub →
Prerequisiti
Prima di creare l'app, devi avere installato Angular CLI. Lo useremo per inizializzare e impalcare l'app. Se non l'hai ancora installato, puoi ottenerlo tramite npm.
npm install -g @angular/cli
Avrai anche bisogno di un account sviluppatore Commerce Layer. Utilizzando l'account sviluppatore, dovrai creare un'organizzazione di test e inviarla con i dati di test. Il seeding semplifica lo sviluppo iniziale dell'app senza preoccuparsi dei dati che dovrai utilizzare. Puoi creare un account a questo link e un'organizzazione qui.
Infine, avrai bisogno di un account Sandbox Paypal. Avere questo tipo di account ci consentirà di testare le transazioni tra aziende e utenti senza rischiare denaro reale. Puoi crearne uno qui. Un account sandbox ha un'attività di prova e un account personale di prova già creato per essa.
Commerce Layer e configurazione Paypal
Per rendere possibili i pagamenti Paypal Sandbox su Commerce Layer, dovrai impostare le chiavi API. Vai alla panoramica degli account del tuo account sviluppatore Paypal. Seleziona un account aziendale e nella scheda Credenziali API dei dettagli dell'account, troverai l' applicazione predefinita in App REST .
Per associare il tuo account aziendale Paypal alla tua organizzazione Commerce Layer, vai alla dashboard della tua organizzazione. Qui aggiungerai un gateway di pagamento Paypal e un metodo di pagamento Paypal per i tuoi vari mercati. In Impostazioni > Pagamenti , seleziona Gateway di pagamento > Paypal e aggiungi il tuo ID cliente e segreto Paypal.
Dopo aver creato il gateway, dovrai creare un metodo di pagamento Paypal per ogni mercato a cui ti rivolgi per rendere Paypal disponibile come opzione. Lo farai in Impostazioni > Pagamenti > Metodi di pagamento > Nuovo metodo di pagamento .
Una nota sulle rotte utilizzate
Commerce Layer fornisce un percorso per l'autenticazione e un altro diverso insieme di percorsi per la loro API. Il loro percorso di autenticazione /oauth/token
scambia le credenziali con un token. Questo token è necessario per accedere alla loro API. Il resto dei percorsi API prende il modello /api/:resource
.
L'ambito di questo articolo copre solo la parte front-end di questa app. Ho scelto di archiviare i token lato server, utilizzare le sessioni per tenere traccia della proprietà e fornire cookie solo http con un ID sessione al client. Questo non sarà trattato qui in quanto non rientra nell'ambito di questo articolo. Tuttavia, i percorsi rimangono gli stessi e corrispondono esattamente all'API Commerce Layer. Tuttavia, ci sono un paio di percorsi personalizzati non disponibili dall'API Commerce Layer che utilizzeremo. Questi riguardano principalmente la gestione delle sessioni. Li indicherò man mano che li raggiungiamo e descriverò come puoi ottenere un risultato simile.
Un'altra incoerenza che potresti notare è che i corpi delle richieste differiscono da quelli richiesti dall'API Commerce Layer. Poiché le richieste vengono inoltrate a un altro server per essere popolate con un token, ho strutturato i corpi in modo diverso. Questo per facilitare l'invio delle richieste. Eventuali incongruenze negli organi di richiesta verranno segnalate nei servizi.
Poiché questo è fuori portata, dovrai decidere come archiviare i token in modo sicuro. Dovrai anche modificare leggermente i corpi delle richieste in modo che corrispondano esattamente a ciò che richiede l'API Commerce Layer. Quando c'è un'incoerenza, mi collegherò al riferimento dell'API e alle guide che descrivono in dettaglio come strutturare correttamente il corpo.
Struttura dell'app
Per organizzare l'app, la suddivideremo in quattro parti principali. Una migliore descrizione di ciò che ciascuno dei moduli fa è fornita nelle sezioni corrispondenti:
- il modulo centrale,
- il modulo dati,
- il modulo condiviso,
- i moduli di funzionalità.
I moduli di funzionalità raggrupperanno insieme pagine e componenti correlati. Ci saranno quattro moduli di funzionalità:
- il modulo di autorizzazione,
- il modulo del prodotto,
- il modulo carrello,
- il modulo di cassa.
Man mano che arriviamo a ciascun modulo, spiegherò qual è il suo scopo e analizzerò il suo contenuto.
Di seguito è riportato un albero della cartella src/app
e dove risiede ogni modulo.
src ├── app │ ├── core │ ├── data │ ├── features │ │ ├── auth │ │ ├── cart │ │ ├── checkout │ │ └── products └── shared
Generazione dell'app e aggiunta di dipendenze
Inizieremo generando l'app. La nostra organizzazione si chiamerà The LIme Brand e disporrà di dati di test già seminati da Commerce Layer.
ng new lime-app
Avremo bisogno di un paio di dipendenze. Principalmente materiale angolare e fino alla distruzione. Il materiale angolare fornirà componenti e stile. Fino a quando Destroy annulla automaticamente l'iscrizione agli osservabili quando i componenti vengono distrutti. Per installarli esegui:
npm install @ngneat/until-destroy ng add @angular/material
Risorse
Quando si aggiungono indirizzi a Commerce Layer, è necessario utilizzare un codice paese alfa-2. Aggiungeremo un file json contenente questi codici alla cartella assets
in assets/json/country-codes.json
. Puoi trovare questo file collegato qui.
Stili
I componenti che creeremo condividono uno stile globale. Li inseriremo in styles.css
che può essere trovato a questo link.
Ambiente
La nostra configurazione sarà composta da due campi. L' apiUrl
che dovrebbe puntare all'API Commerce Layer. apiUrl
viene utilizzato dai servizi che creeremo per recuperare i dati. clientUrl
deve essere il dominio su cui è in esecuzione l'app. Lo usiamo quando impostiamo gli URL di reindirizzamento per Paypal. Potete trovare questo file a questo link.
Modulo condiviso
Il modulo condiviso conterrà servizi, pipe e componenti condivisi tra gli altri moduli.
ng gm shared
Si compone di tre componenti, un tubo e due servizi. Ecco come sarà.
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
Utilizzeremo anche il modulo condiviso per esportare alcuni componenti di materiale angolare comunemente usati. Ciò rende più facile utilizzarli immediatamente invece di importare ogni componente su vari moduli. Ecco cosa shared.module.ts
.
@NgModule({ declarations: [SimplePageComponent, TitleComponent, WordWrapPipe, ItemQuantityComponent], imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, RouterModule], exports: [ CommonModule, ItemQuantityComponent, MatButtonModule, MatIconModule, MatSnackBarModule, MatTooltipModule, SimplePageComponent, TitleComponent, WordWrapPipe ] }) export class SharedModule { }
Componenti
Componente quantità articolo
Questo componente imposta la quantità di articoli quando li aggiungi al carrello. Verrà utilizzato nel carrello e nei moduli dei prodotti. Un selettore di materiale sarebbe stata una scelta facile per questo scopo. Tuttavia, lo stile della selezione del materiale non corrispondeva agli input di materiale utilizzati in tutte le altre forme. Un menu materiale sembrava molto simile agli input materiali utilizzati. Quindi ho deciso di creare un componente selezionato con esso.
ng gc shared/components/item-quantity
Il componente avrà tre proprietà di input e una proprietà di output. quantity
imposta la quantità iniziale di articoli, maxValue
indica il numero massimo di articoli che possono essere selezionati in una volta sola e disabled
indica se il componente deve essere disabilitato o meno. Il setQuantityEvent
viene attivato quando viene selezionata una quantità.
Quando il componente viene inizializzato, imposteremo i valori che appaiono nel menu del materiale. Esiste anche un metodo chiamato setQuantity
che emetterà eventi setQuantityEvent
.
Questo è il file del componente.
@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); } }
Questo è il suo modello.
<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>
Ecco il suo stile.
button { margin: 3px; }
Componente del titolo
Questo componente funge anche da titolo stepper e titolo semplice su alcune pagine più semplici. Sebbene Angular Material fornisca un componente stepper, non era la soluzione migliore per un processo di checkout piuttosto lungo, non era così reattivo su display più piccoli e richiedeva molto più tempo per l'implementazione. Un titolo più semplice, tuttavia, potrebbe essere riproposto come indicatore stepper ed essere utile su più pagine.
ng gc shared/components/title
Il componente ha quattro proprietà di input: un title
, un subtitle
, un numero ( no
) e centerText
, per indicare se centrare il testo del componente.
@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; }
Di seguito è riportato il suo modello. Puoi trovare il suo stile collegato qui.
<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>
Componente di pagina semplice
Esistono più casi in cui un titolo, un'icona e un pulsante erano tutto ciò che era necessario per una pagina. Questi includono una pagina 404, una pagina del carrello vuota, una pagina di errore, una pagina di pagamento e una pagina di posizionamento dell'ordine. Questo è lo scopo che servirà il componente della pagina semplice. Quando si fa clic sul pulsante nella pagina, verrà reindirizzato a un percorso o verrà eseguita un'azione in risposta a un buttonEvent
.
Per farlo:
ng gc shared/components/simple-page
Questo è il suo file componente.
@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(); } } }
E il suo modello contiene:
<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>
Il suo stile può essere trovato qui.
Tubi
Tubo per avvolgere le parole
I nomi di alcuni prodotti e altri tipi di informazioni visualizzate sul sito sono davvero lunghi. In alcuni casi, è difficile inserire queste lunghe frasi in componenti materiali. Quindi useremo questa pipe per ridurre le frasi a una lunghezza specificata e aggiungere ellissi alla fine del risultato.
Per crearlo esegui:
ng g pipe shared/pipes/word-wrap
Conterrà:
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)}...`; } }
Servizi
Servizio di gestione degli errori HTTP
Ci sono un certo numero di servizi http in questo progetto. La creazione di un gestore degli errori per ogni metodo è ripetitiva. Quindi ha senso creare un unico gestore che può essere utilizzato da tutti i metodi. Il gestore degli errori può essere utilizzato per formattare un errore e anche trasmetterli ad altre piattaforme di registrazione esterne.
Generalo eseguendo:
ng gs shared/services/http-error-handler
Questo servizio conterrà un solo metodo. Il metodo formatterà il messaggio di errore da visualizzare a seconda che si tratti di un errore del client o del server. Tuttavia, c'è spazio per migliorarlo ulteriormente.
@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); } }
Servizio di archiviazione locale
Utilizzeremo l'archiviazione locale per tenere traccia del numero di articoli in un carrello. È anche utile memorizzare qui l'ID di un ordine. Un ordine corrisponde a un carrello su Commerce Layer.
Per generare il servizio di archiviazione locale, eseguire:
ng gs shared/services/local-storage
Il servizio conterrà quattro metodi per aggiungere, eliminare e ottenere elementi dalla memoria locale e un altro per cancellarli.
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(); } }
Modulo Dati
Questo modulo è responsabile del recupero e della gestione dei dati. È ciò che useremo per ottenere i dati consumati dalla nostra app. Di seguito la sua struttura:
src/app/data ├── data.module.ts ├── models └── services
Per generare l'esecuzione del modulo:
ng gm data
Modelli
I modelli definiscono come sono strutturati i dati che consumiamo dall'API. Avremo 16 dichiarazioni di interfaccia. Per crearli esegui:
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
La tabella seguente si collega a ciascun file e fornisce una descrizione di ogni interfaccia.
Interfaccia | Descrizione |
---|---|
Indirizzo | Rappresenta un indirizzo generale. |
Carrello | Versione lato client di un ordine che tiene traccia del numero di prodotti che un cliente intende acquistare. |
Paese | Codice paese alfa-2. |
Indirizzo del cliente | Un indirizzo associato a un cliente. |
Cliente | Un utente registrato. |
Tempi di consegna | Rappresenta il tempo necessario per la consegna di una spedizione. |
Elemento pubblicitario | Un prodotto dettagliato aggiunto al carrello. |
Ordine | Un carrello degli acquisti o una raccolta di elementi pubblicitari. |
Metodo di pagamento | Un tipo di pagamento reso disponibile per un ordine. |
Fonte di pagamento | Un pagamento associato a un ordine. |
Pagamento Paypal | Un pagamento effettuato tramite Paypal |
Prezzo | Prezzo associato a uno SKU. |
Spedizione | Raccolta di articoli spediti insieme. |
metodo di spedizione | Metodo attraverso il quale viene spedito un pacco. |
SKU | Un'unità di scorta unica. |
Posizione delle scorte | Posizione che contiene l'inventario SKU. |
Servizi
Questa cartella contiene i servizi che creano, recuperano e manipolano i dati dell'app. Creeremo 11 servizi qui.
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
Servizio di indirizzo
Questo servizio crea e recupera indirizzi. È importante durante la creazione e l'assegnazione di indirizzi di spedizione e fatturazione agli ordini. Ha due metodi. Uno per creare un indirizzo e un altro per recuperarne uno.
Il percorso utilizzato qui è /api/addresses
. Se intendi utilizzare direttamente l'API Commerce Layer, assicurati di strutturare i dati come illustrato in questo esempio.
@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)); } }
Servizio carrello
Il carrello è responsabile del mantenimento della quantità di articoli aggiunti e dell'ID ordine. Effettuare chiamate API per ottenere il numero di articoli in un ordine ogni volta che viene creato un nuovo elemento pubblicitario può essere costoso. Invece, potremmo semplicemente utilizzare l'archiviazione locale per mantenere il conteggio sul client. Ciò elimina la necessità di recuperare gli ordini non necessari ogni volta che un articolo viene aggiunto al carrello.
Utilizziamo questo servizio anche per memorizzare l'ID dell'ordine. Un carrello corrisponde a un ordine su Commerce Layer. Una volta aggiunto il primo articolo al carrello, viene creato un ordine. Dobbiamo preservare questo ID ordine in modo da poterlo recuperare durante il processo di pagamento.
Inoltre, abbiamo bisogno di un modo per comunicare all'intestazione che un articolo è stato aggiunto al carrello. L'intestazione contiene il pulsante del carrello e mostra la quantità di articoli in esso contenuti. Useremo un osservabile di un BehaviorSubject
con il valore corrente del carrello. L'intestazione può iscriversi a questo e tenere traccia delle modifiche nel valore del carrello.
Infine, una volta che un ordine è stato completato, il valore del carrello deve essere cancellato. Ciò garantisce che non ci sia confusione durante la creazione di ordini successivi più recenti. I valori che sono stati memorizzati vengono cancellati una volta che l'ordine corrente viene contrassegnato come effettuato.
Faremo tutto questo utilizzando il servizio di archiviazione locale creato in precedenza.
@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 }); } }
Servizio Paese
Quando si aggiungono indirizzi su Commerce Layer, il codice paese deve essere un codice alfa 2. Questo servizio legge un file json contenente questi codici per ogni paese e lo restituisce nel metodo 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'); } }
Servizio di indirizzo del cliente
Questo servizio viene utilizzato per associare indirizzi ai clienti. Recupera anche uno specifico o tutti gli indirizzi relativi a un cliente. Viene utilizzato quando il cliente aggiunge i propri indirizzi di spedizione e fatturazione al proprio ordine. Il metodo createCustomer
crea un cliente, getCustomerAddresses
ottiene tutti gli indirizzi di un cliente e getCustomerAddress
ottiene uno specifico.
Quando crei un indirizzo cliente, assicurati di strutturare il corpo della posta secondo questo esempio.
@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)); } }
Assistenza clienti
I clienti vengono creati e le loro informazioni recuperate utilizzando questo servizio. Quando un utente si registra, diventa un cliente e viene creato utilizzando createCustomerMethod
. getCustomer
restituisce il cliente associato a un ID specifico. getCurrentCustomer
restituisce il cliente attualmente connesso.
Quando crei un cliente, struttura i dati in questo modo. Puoi aggiungere i loro nomi e cognomi ai metadati, come mostrato nei relativi attributi.
Il percorso /api/customers/current
non è disponibile su Commerce Layer. Quindi dovrai capire come ottenere il cliente attualmente connesso.
@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)); } }
Servizio tempi di consegna
Questo servizio restituisce informazioni sulle tempistiche di spedizione da varie posizioni di magazzino.
@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)); } }
Servizio di elementi pubblicitari
Gli articoli aggiunti al carrello sono gestiti da questo servizio. Con esso, puoi creare un articolo nel momento in cui viene aggiunto al carrello. È anche possibile recuperare le informazioni su un elemento. L'articolo può anche essere aggiornato quando la sua quantità cambia o cancellato quando viene rimosso dal carrello.
Quando si creano elementi o li si aggiorna, strutturare il corpo della richiesta come mostrato in questo esempio.
@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)); } }
Servizio ordini
Analogamente al servizio elementi pubblicitari, il servizio ordini consente di creare, aggiornare, eliminare o ricevere un ordine. Inoltre, puoi scegliere di ricevere le spedizioni associate a un ordine separatamente utilizzando il metodo getOrderShipments
. Questo servizio viene utilizzato pesantemente durante il processo di pagamento.
Ci sono diversi tipi di informazioni su un ordine che sono richieste durante il checkout. Poiché può essere costoso recuperare un intero ordine e le sue relazioni, specifichiamo cosa vogliamo ottenere da un ordine usando GetOrderParams
. L'equivalente di questo nell'API CL è il parametro di query include in cui vengono elencate le relazioni di ordine da includere. Puoi controllare quali campi devono essere inclusi per il riepilogo del carrello qui e per le varie fasi di pagamento qui.
Allo stesso modo, quando aggiorniamo un ordine, utilizziamo UpdateOrderParams
per specificare i campi di aggiornamento. Questo perché nel server che popola il token, vengono eseguite alcune operazioni extra a seconda del campo che viene aggiornato. Tuttavia, se stai effettuando richieste dirette all'API CL, non è necessario specificarlo. Puoi eliminarlo poiché l'API CL non richiede di specificarli. Anche se il corpo della richiesta dovrebbe essere simile a questo esempio.
@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)); } }
Servizio di pagamento Paypal
Questo servizio è responsabile della creazione e dell'aggiornamento dei pagamenti Paypal per gli ordini. Inoltre, possiamo ottenere un pagamento Paypal dato il suo ID. Il corpo della posta dovrebbe avere una struttura simile a questo esempio durante la creazione di un pagamento 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)); } }
Servizio di spedizione
Questo servizio riceve una spedizione o la aggiorna in base al suo ID. Il corpo della richiesta di un aggiornamento della spedizione dovrebbe essere simile a questo esempio.
@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)); } }
Servizio SKU
Il servizio SKU riceve i prodotti dal negozio. Se vengono recuperati più prodotti, è possibile impaginarli e impostare una dimensione della pagina. La dimensione della pagina e il numero di pagina devono essere impostati come parametri di query come in questo esempio se stai effettuando richieste dirette all'API. È anche possibile recuperare un singolo prodotto in base al suo ID.
@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)); } }
Modulo principale
Il modulo principale contiene tutto ciò che è centrale e comune all'applicazione. Questi includono componenti come l'intestazione e pagine come la pagina 404. Anche i servizi responsabili dell'autenticazione e della gestione delle sessioni rientrano qui, così come gli intercettori e le guardie a livello di app.
L'albero del modulo principale sarà simile a questo.
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
Per generare il modulo e il suo contenuto, eseguire:
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
Il file del modulo principale dovrebbe essere simile a questo. Si noti che le rotte sono state registrate per NotFoundComponent
e 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 { }
Servizi
La cartella dei servizi contiene i servizi di autenticazione, sessione e intestazione.
Servizio di autenticazione
Il servizio di AuthenticationService
consente di acquisire client e token cliente. Questi token vengono utilizzati per accedere al resto dei percorsi dell'API. I token cliente vengono restituiti quando un utente scambia un'e-mail e una password e dispone di una gamma più ampia di autorizzazioni. I token client vengono emessi senza la necessità di credenziali e dispongono di autorizzazioni più limitate.
getClientSession
ottiene un token client. login
ottiene un token cliente. Entrambi i metodi creano anche una sessione. Il corpo di una richiesta di token del cliente dovrebbe assomigliare a questo e quello di un token del cliente come questo.
@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); } }
Componenti
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.
Esiste un metodo di logout
che distrugge la sessione di un utente e assegna loro un token client. Viene assegnato un token client perché la sessione che mantiene il token del cliente viene distrutta ed è ancora necessario un token per ogni richiesta API. Uno snackbar materiale comunica all'utente se la sua sessione è stata distrutta con successo o meno.
Usiamo il @UntilDestroy({ checkProperties: true })
per indicare che tutte le sottoscrizioni devono essere automaticamente annullate da quando il componente viene distrutto.
@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 }) ); } }
Di seguito è riportato il modello di intestazione e collegato qui è il suo stile.
<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>
Guardie ✔
Protezione del carrello vuota
Questa protezione impedisce agli utenti di accedere ai percorsi relativi al checkout e alla fatturazione se il carrello è vuoto. Questo perché per procedere con il checkout è necessario che ci sia un ordine valido. Un ordine corrisponde a un carrello con degli articoli al suo interno. Se sono presenti articoli nel carrello, l'utente può procedere a una pagina protetta. Tuttavia, se il carrello è vuoto, l'utente viene reindirizzato a una pagina del carrello vuota.
@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'); } }
Intercettori
Opzioni Intercettore
Questo intercettore intercetta tutte le richieste HTTP in uscita e aggiunge due opzioni alla richiesta. Si tratta di un'intestazione Content-Type
e di una proprietà withCredentials
. withCredentials
specifica se una richiesta deve essere inviata con credenziali in uscita come i cookie solo http che utilizziamo. Usiamo Content-Type
per indicare che stiamo inviando risorse json al server.
@Injectable() export class OptionsInterceptor implements HttpInterceptor { constructor() { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json'), withCredentials: true }); return next.handle(request); } }
Moduli di funzionalità
Questa sezione contiene le caratteristiche principali dell'app. Come accennato in precedenza, le funzionalità sono raggruppate in quattro moduli: moduli di autenticazione, prodotto, carrello e pagamento.
Modulo Prodotti
Il modulo prodotti contiene pagine che mostrano i prodotti in vendita. Questi includono la pagina del prodotto e la pagina dell'elenco dei prodotti. È strutturato come mostrato di seguito.
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
Per generarlo e i suoi componenti:
ng gm features/products ng gc features/products/pages/product ng gc features/products/pages/product-list
Questo è il file del modulo:
@NgModule({ declarations: [ProductListComponent, ProductComponent], imports: [ RouterModule.forChild([ { path: 'product/:id', component: ProductComponent }, { path: '', component: ProductListComponent } ]), LayoutModule, MatCardModule, MatGridListModule, MatPaginatorModule, SharedModule ] }) export class ProductsModule { }
Componente elenco prodotti
Questo componente visualizza un elenco impaginato di prodotti disponibili per la vendita. È la prima pagina che viene caricata all'avvio dell'app.
I prodotti vengono visualizzati in una griglia. L'elenco della griglia dei materiali è il componente migliore per questo. Per rendere la griglia reattiva, il numero di colonne della griglia cambierà in base alle dimensioni dello schermo. Il servizio BreakpointObserver
ci permette di determinare la dimensione dello schermo e di assegnare le colonne durante l'inizializzazione.
Per ottenere i prodotti, chiamiamo il metodo getProducts
di SkuService
. Restituisce i prodotti in caso di esito positivo e li assegna alla griglia. In caso contrario, indirizziamo l'utente alla pagina di errore.
Poiché i prodotti visualizzati sono impaginati, avremo un metodo getNextPage
per ottenere i prodotti aggiuntivi.
@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}`; } }
Il modello è mostrato di seguito e il suo stile può essere trovato qui.
<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 pagina sarà simile a questa.
Componente del prodotto
Dopo aver selezionato un prodotto dalla pagina dell'elenco dei prodotti, questo componente ne mostra i dettagli. Questi includono il nome completo, il prezzo e la descrizione del prodotto. C'è anche un pulsante per aggiungere l'articolo al carrello del prodotto.
All'inizializzazione, otteniamo l'id del prodotto dai parametri del percorso. Usando l'id, recuperiamo il prodotto da SkuService
.
Quando l'utente aggiunge un articolo al carrello, viene chiamato il metodo addItemToCart
. In esso, controlliamo se è già stato creato un ordine per il carrello. In caso contrario, ne viene creato uno nuovo utilizzando OrderService
. Dopodiché, viene creato un elemento pubblicitario nell'ordine corrispondente al prodotto. Se esiste già un ordine per il carrello, viene creato solo l'elemento pubblicitario. A seconda dello stato delle richieste, all'utente viene visualizzato un messaggio snackbar.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product', templateUrl: './product.component.html', styleUrls: ['./product.component.css'] }) export class ProductComponent implements OnInit { id: string = ''; product!: Sku; quantity: number = 0; constructor( private route: ActivatedRoute, private skus: SkuService, private location: Location, private router: Router, private header: HeaderService, private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.paramMap .pipe( mergeMap(params => { const id = params.get('id') this.id = id ? id : ''; return this.skus.getSku(this.id); }), tap((sku) => { this.product = sku; }) ).subscribe({ error: (err) => this.router.navigateByUrl('/error') }); this.header.setHeaderButtonsVisibility(true); } addItemToCart() { if (this.quantity > 0) { if (this.cart.orderId == '') { this.orders.createOrder() .pipe( mergeMap((order: Order) => { this.cart.orderId = order.id || ''; return this.lineItems.createLineItem({ orderId: order.id, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }); }) ) .subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } else { this.lineItems.createLineItem({ orderId: this.cart.orderId, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }).subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } } else { this.snackBar.open('Select a quantity greater than 0.', 'Close', { duration: 8000 }); } } setQuantity(no: number) { this.quantity = no; } goBack() { this.location.back(); } private showSuccessSnackBar() { this.snackBar.open('Item successfully added to cart.', 'Close', { duration: 8000 }); } private showErrorSnackBar() { this.snackBar.open('Failed to add your item to the cart.', 'Close', { duration: 8000 }); } }
Il modello ProductComponent
è il seguente e il suo stile è collegato qui.
<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 pagina sarà simile a questa.
Modulo di autorizzazione
Il modulo Auth contiene le pagine responsabili dell'autenticazione. Questi includono le pagine di accesso e di registrazione. È strutturato come segue.
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
Per generarlo e i suoi componenti:
ng gm features/auth ng gc features/auth/pages/signup ng gc features/auth/pages/login
Questo è il suo file di modulo.
@NgModule({ declarations: [LoginComponent, SignupComponent], imports: [ RouterModule.forChild([ { path: 'login', component: LoginComponent }, { path: 'signup', component: SignupComponent } ]), MatFormFieldModule, MatInputModule, ReactiveFormsModule, SharedModule ] }) export class AuthModule { }
Componente di iscrizione
Un utente registra un account utilizzando questo componente. Per il processo sono richiesti nome, cognome, e-mail e password. L'utente deve anche confermare la propria password. I campi di input verranno creati con il servizio FormBuilder
. La convalida viene aggiunta per richiedere che tutti gli input abbiano valori. Viene aggiunta un'ulteriore convalida al campo della password per garantire una lunghezza minima di otto caratteri. Un validatore matchPasswords
personalizzato assicura che la password confermata corrisponda alla password iniziale.
Quando il componente viene inizializzato, i pulsanti carrello, login e logout nell'intestazione sono nascosti. Ciò viene comunicato all'intestazione utilizzando HeaderService
.
Dopo che tutti i campi sono stati contrassegnati come validi, l'utente può quindi registrarsi. Nel metodo di signup
, il metodo createCustomer
di CustomerService
riceve questo input. Se la registrazione ha esito positivo, l'utente viene informato che il proprio account è stato creato correttamente utilizzando uno snackbar. Vengono quindi reindirizzati alla home page.
@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 }) ); } }
Di seguito è riportato il modello per il 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>
Il componente risulterà come segue.
Componente di accesso
Un utente registrato accede al proprio account con questo componente. È necessario inserire un'e-mail e una password. I loro campi di input corrispondenti avrebbero una convalida che li rende obbligatori.
Simile a SignupComponent
, i pulsanti carrello, login e logout nell'intestazione sono nascosti. La loro visibilità viene impostata utilizzando HeaderService
durante l'inizializzazione del componente.
Per effettuare il login, le credenziali vengono passate al AuthenticationService
. In caso di esito positivo, lo stato di accesso dell'utente viene impostato utilizzando SessionService
. L'utente viene quindi reindirizzato alla pagina in cui si trovava. In caso di esito negativo, viene visualizzata una snackbar con un errore e il campo della password viene reimpostato.
@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: '' }); } ); } }
Di seguito è riportato il modello 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>
Ecco uno screenshot della pagina.
Modulo carrello
Il modulo carrello contiene tutte le pagine relative al carrello. Questi includono la pagina di riepilogo dell'ordine, una pagina del codice del coupon e della carta regalo e una pagina del carrello vuota. È strutturato come segue.
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
Per generarlo, esegui:
ng gm features/cart ng gc features/cart/codes ng gc features/cart/empty ng gc features/cart/summary
Questo è il file del modulo.
@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 { }
Codici Componente
Come accennato in precedenza, questo componente viene utilizzato per aggiungere eventuali codici coupon o buoni regalo a un ordine. Ciò consente all'utente di applicare sconti al totale del proprio ordine prima di procedere al checkout.
Ci saranno due campi di input. Uno per i coupon e un altro per i codici delle carte regalo.
I codici vengono aggiunti aggiornando l'ordine. Il metodo updateOrder
di OrderService
aggiorna l'ordine con i codici. Dopodiché, entrambi i campi vengono ripristinati e l'utente viene informato del successo dell'operazione con uno snack bar. Quando si verifica un errore viene visualizzato anche uno snackbar. Entrambi i metodi addCoupon
e addGiftCard
chiamano il metodo 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'); } }
Il modello è mostrato di seguito e il suo stile può essere trovato a questo link.
<div> <app-title title="Redeem a code" subtitle="Enter a coupon code or gift card" [centerText]="true"></app-title> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Coupon Code</mat-label> <input matInput [formControl]="couponCode" required> <mat-icon matPrefix>card_giftcard</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="couponCode.invalid" (click)="addCoupon()">Redeem</button> </div> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Gift Card Code</mat-label> <input matInput [formControl]="giftCardCode" required> <mat-icon matPrefix>redeem</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="giftCardCode.invalid" (click)="addGiftCard()">Redeem</button> </div> <button color="primary" mat-flat-button routerLink="/cart"> <mat-icon>shopping_cart</mat-icon> CONTINUE TO CART </button> </div>
Ecco uno screenshot della pagina.
Componente vuoto
Non dovrebbe essere possibile effettuare il check-out con un carrello vuoto. È necessario che ci sia una protezione che impedisca agli utenti di accedere alle pagine del modulo di pagamento con carrelli vuoti. Questo è già stato trattato come parte del CoreModule
. La guardia reindirizza le richieste alle pagine di pagamento con un carrello vuoto al EmptyCartComponent
.
È un componente molto semplice che ha del testo che indica all'utente che il suo carrello è vuoto. Ha anche un pulsante su cui l'utente può fare clic per andare alla home page per aggiungere cose al carrello. Quindi useremo SimplePageComponent
per visualizzarlo. Ecco il modello.
<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>
Ecco uno screenshot della pagina.
Componente di riepilogo
Questo componente riassume il carrello/ordine. Elenca tutti gli articoli nel carrello, i loro nomi, quantità e immagini. Scompone inoltre il costo dell'ordine inclusi tasse, spedizione e sconti. L'utente dovrebbe essere in grado di visualizzarlo e decidere se è soddisfatto degli articoli e del costo prima di procedere al pagamento.
Al momento dell'inizializzazione, l'ordine e i relativi elementi pubblicitari vengono recuperati utilizzando OrderService
. Un utente dovrebbe essere in grado di modificare gli elementi pubblicitari o addirittura rimuoverli dall'ordine. Gli elementi vengono rimossi quando viene chiamato il metodo deleteLineItem
. In esso il metodo deleteLineItem
di LineItemService
riceve l'id dell'elemento pubblicitario da eliminare. Se l'eliminazione va a buon fine, aggiorniamo il conteggio degli articoli nel carrello utilizzando il CartService
.
L'utente viene quindi indirizzato alla pagina del cliente dove inizia il processo di check-out. Il metodo di checkout
esegue l'instradamento.
@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') ); } }
Di seguito è riportato il modello e il suo stile è collegato qui.
<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>
Ecco uno screenshot della pagina.
Modulo di pagamento
Questo modulo è responsabile del processo di checkout. Il checkout comporta la fornitura di un indirizzo di fatturazione e spedizione, un'e-mail del cliente e la selezione di un metodo di spedizione e pagamento. L'ultimo passaggio di questo processo è l'inserimento e la conferma dell'ordine. La struttura del modulo è la seguente.
src/app/features/checkout/ ├── components │ ├── address │ ├── address-list │ └── country-select └── pages ├── billing-address ├── cancel-payment ├── customer ├── payment ├── place-order ├── shipping-address └── shipping-methods
Questo modulo è di gran lunga il più grande e contiene 3 componenti e 7 pagine. Per generarlo e i suoi componenti eseguire:
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
Questo è il file del modulo.
@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 { }
Componenti
Paese Seleziona componente
Questo componente consente a un utente di selezionare un paese come parte di un indirizzo. Il componente di selezione del materiale ha un aspetto piuttosto diverso rispetto ai campi di input nel modulo dell'indirizzo. Quindi, per motivi di uniformità, viene invece utilizzato un componente del menu materiale.
Quando il componente viene inizializzato, i dati del codice paese vengono recuperati utilizzando CountryService
. La proprietà countries
contiene i valori restituiti dal servizio. Questi valori verranno aggiunti al menu nel modello.
Il componente ha una proprietà di output, setCountryEvent
. Quando viene selezionato un paese, questo evento emette il codice alfa-2 del paese.
@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); }}
Di seguito è riportato il suo modello e collegato qui è il suo stile.
<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>
Componente indirizzo
Questo è un modulo per acquisire indirizzi. Viene utilizzato sia dalla pagina dell'indirizzo di spedizione che da quella di fatturazione. Un indirizzo Commerce Layer valido deve contenere un nome e un cognome, una riga dell'indirizzo, una città, un codice postale, un prefisso statale, un prefisso internazionale e un numero di telefono.
Il servizio FormBuilder
creerà il gruppo di moduli. Poiché questo componente viene utilizzato da più pagine, dispone di numerose proprietà di input e output. Le proprietà di input includono il testo del pulsante, il titolo visualizzato e il testo di una casella di controllo. Le proprietà di output saranno emettitori di eventi per quando si fa clic sul pulsante per creare l'indirizzo e un altro per quando il valore della casella di controllo cambia.
Quando si fa clic sul pulsante, viene chiamato il metodo addAddress
e l'evento createAddress
emette l'indirizzo completo. Allo stesso modo, quando la casella di controllo è selezionata, l'evento isCheckboxChecked
emette il valore della casella di controllo.
@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); } }
Ecco il suo modello. Puoi trovare il suo stile qui.
<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>
Pagine
Componente del cliente
Un ordine deve essere associato a un indirizzo email. Questo componente è un modulo che acquisisce l'indirizzo e-mail del cliente. Quando il componente viene inizializzato, l'indirizzo e-mail del cliente corrente viene recuperato se ha effettuato l'accesso. Riceviamo il cliente dal CustomerService
. Se non desiderano modificare il loro indirizzo e-mail, questa e-mail sarà il valore predefinito.
Se l'e-mail viene modificata o un cliente non ha effettuato l'accesso, l'ordine viene aggiornato con l'e-mail inserita. Utilizziamo OrderService
per aggiornare l'ordine con il nuovo indirizzo e-mail. In caso di esito positivo, indirizzeremo il cliente alla pagina dell'indirizzo di fatturazione.
@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 }) ); } }
Ecco il modello del componente e collegato qui è il suo stile.
<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>
Ecco uno screenshot della pagina del cliente.
Componente indirizzo di fatturazione
Il componente dell'indirizzo di fatturazione consente a un cliente di aggiungere un nuovo indirizzo di fatturazione o di scegliere dagli indirizzi esistenti. Gli utenti che non hanno effettuato l'accesso devono inserire un nuovo indirizzo. Coloro che hanno effettuato l'accesso hanno la possibilità di scegliere tra indirizzi nuovi o esistenti.
La proprietà showAddress
indica se gli indirizzi esistenti devono essere visualizzati sul componente. sameShippingAddressAsBilling
indica se l'indirizzo di spedizione deve coincidere con l'indirizzo di fatturazione impostato. Quando un cliente seleziona un indirizzo esistente, il suo ID viene assegnato a selectedCustomerAddressId
.
Quando il componente viene inizializzato, utilizziamo SessionService
per verificare se l'utente corrente ha effettuato l'accesso. Se ha effettuato l'accesso, visualizzeremo i loro indirizzi esistenti, se presenti.
Come accennato in precedenza, se un utente ha effettuato l'accesso, può scegliere un indirizzo esistente come indirizzo di fatturazione. Nel metodo updateBillingAddress
, se hanno effettuato l'accesso, l'indirizzo selezionato viene clonato e impostato come indirizzo di fatturazione dell'ordine. Lo facciamo aggiornando l'ordine utilizzando il metodo updateOrder
di OrderService
e fornendo l'indirizzo Id.
Se non sono registrati, l'utente deve fornire un indirizzo. Una volta fornito, l'indirizzo viene creato utilizzando il metodo createAddress
. In esso, AddressService
prende l'input e crea il nuovo indirizzo. Dopodiché, l'ordine viene aggiornato utilizzando l'id dell'indirizzo appena creato. Se si verifica un errore o una delle due operazioni ha esito positivo, viene mostrata una snackbar.
Se viene selezionato lo stesso indirizzo come indirizzo di spedizione, l'utente viene indirizzato alla pagina dei metodi di spedizione. Se desiderano fornire un indirizzo di spedizione alternativo, vengono indirizzati alla pagina dell'indirizzo di spedizione.
@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'); } } }
Ecco il modello. Questo collegamento punta al suo stile.
<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>
Ecco come apparirà la pagina dell'indirizzo di fatturazione.
Componente indirizzo di spedizione
Il componente dell'indirizzo di spedizione si comporta in modo molto simile al componente dell'indirizzo di fatturazione. Tuttavia, ci sono un paio di differenze. Per uno, il testo visualizzato sul modello è diverso. Le altre differenze principali riguardano il modo in cui l'ordine viene aggiornato utilizzando OrderService
una volta creato o selezionato un indirizzo. I campi che l'ordine aggiorna sono shippingAddressCloneId
per gli indirizzi selezionati e shippingAddress
per i nuovi indirizzi. Se un utente sceglie di modificare l'indirizzo di fatturazione in modo che corrisponda all'indirizzo di spedizione, il campo billingAddressSameAsShipping
viene aggiornato.
Dopo aver selezionato un indirizzo di spedizione e aggiornato l'ordine, l'utente viene indirizzato alla pagina dei metodi di spedizione.
@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); } }
Ecco il modello e il suo stile può essere trovato qui.
<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 pagina dell'indirizzo di spedizione sarà simile a questa.
Componente Metodi di spedizione
Questo componente mostra il numero di spedizioni necessarie per l'evasione di un ordine, i metodi di spedizione disponibili e i relativi costi. Il cliente può quindi selezionare un metodo di spedizione che preferisce per ogni spedizione.
La proprietà delle shipments
contiene tutte le spedizioni dell'ordine. Il modulo shipmentsForm
è il modulo all'interno del quale verranno effettuate le selezioni del metodo di spedizione.
Quando il componente viene inizializzato, l'ordine viene recuperato e conterrà sia gli elementi pubblicitari che le spedizioni. Allo stesso tempo, otteniamo i tempi di consegna per i vari metodi di spedizione. Usiamo OrderService
per ottenere l'ordine e DeliveryLeadTimeService
per i tempi di consegna. Una volta che entrambi i set di informazioni sono stati restituiti, vengono combinati in una matrice di spedizioni e assegnati alla proprietà delle shipments
. Ogni spedizione conterrà i suoi articoli, le modalità di spedizione disponibili e il costo corrispondente.
Dopo che l'utente ha selezionato un metodo di spedizione per ciascuna spedizione, il metodo di spedizione selezionato viene aggiornato per ciascuna in setShipmentMethods
. In caso di esito positivo, l'utente viene indirizzato alla pagina dei pagamenti.
@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]; } }
Ecco il template e potete trovare lo styling a questo link.
<form [formGroup]="shipmentsForm"> <app-title no="4" title="Shipping Methods" subtitle="How to ship your packages"></app-title> <div class="shipment-container" *ngFor="let shipment of shipments; let j = index; let isLast = last"> <h1>Shipment {{j+1}} of {{shipments?.length}}</h1> <div class="row" *ngFor="let item of shipment.lineItems"> <img class="image-xs" [src]="item.imageUrl" alt="product photo"> <div> <h4>{{item.name}}</h4> <p>{{item.skuCode}}</p> </div> <div> <p>Quantity: </p>{{item.quantity}} </div> </div> <mat-radio-group [formControlName]="shipment?.id || j"> <mat-radio-button *ngFor="let method of shipment.availableShippingMethods" [value]="method.id"> <div class="radio-button"> <p>{{method.name}}</p> <div> <p class="radio-label">Cost:</p> <p> {{method.formattedPriceAmount}}</p> </div> <div> <p class="radio-label">Timeline:</p> <p> Available in {{method.deliveryLeadTime?.minDays}}-{{method.deliveryLeadTime?.maxDays}} days</p> </div> </div> </mat-radio-button> </mat-radio-group> <mat-divider *ngIf="!isLast"></mat-divider> </div> <button mat-flat-button color="primary" [disabled]="shipmentsForm.invalid" (click)="setShipmentMethods()">PROCEED TO PAYMENT</button> </form>
Questo è uno screenshot della pagina dei metodi di spedizione.
Componente Pagamenti
In questo componente, l'utente fa clic sul pulsante di pagamento se desidera procedere al pagamento del proprio ordine con Paypal. L' approvalUrl
è il collegamento Paypal a cui viene indirizzato l'utente quando fa clic sul pulsante.
Durante l'inizializzazione, riceviamo l'ordine con la fonte di pagamento inclusa utilizzando OrderService
. Se viene impostata una fonte di pagamento, otteniamo il suo ID e recuperiamo il pagamento Paypal corrispondente da PaypalPaymentService
. Il pagamento Paypal conterrà l'URL di approvazione. Se non è stata impostata alcuna fonte di pagamento, aggiorniamo l'ordine con Paypal come metodo di pagamento preferito. Procediamo quindi a creare un nuovo pagamento Paypal per l'ordine utilizzando il PaypalPaymentService
. Da qui, possiamo ottenere l'URL di approvazione dall'ordine appena creato.
Infine, quando l'utente fa clic sul pulsante, viene reindirizzato a Paypal dove può approvare l'acquisto.
@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; } }
Ecco il suo modello.
<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>
Ecco come apparirà la pagina dei pagamenti.
Annulla componente di pagamento
Paypal richiede una pagina di annullamento del pagamento. Questo componente serve a questo scopo. Questo è il suo modello.
<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>
Ecco uno screenshot della pagina.
Componente dell'ordine
Questo è l'ultimo passaggio della procedura di pagamento. Qui l'utente conferma di voler effettivamente effettuare l'ordine e iniziarne l'elaborazione. Quando l'utente approva il pagamento Paypal, questa è la pagina a cui viene reindirizzato. Paypal aggiunge un parametro di query dell'ID del pagatore all'URL. Questo è l'ID Paypal dell'utente.
Quando il componente viene inizializzato, otteniamo il parametro di query payerId
. L'ordine viene quindi recuperato utilizzando OrderService
con la fonte di pagamento inclusa. L'id della fonte di pagamento inclusa viene utilizzato per aggiornare il pagamento Paypal con l'id del pagatore, utilizzando il servizio PaypalPayment
. Se uno di questi non riesce, l'utente viene reindirizzato alla pagina di errore. Utilizziamo la proprietà disableButton
per impedire all'utente di effettuare l'ordine fino a quando non viene impostato l'ID del pagatore.
Quando fanno clic sul pulsante effettua l'ordine, l'ordine viene aggiornato con uno stato placed
. Dopodiché il carrello viene svuotato, viene visualizzato uno snack bar riuscito e l'utente viene reindirizzato alla home page.
@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; } ); } }
Ecco il modello e lo stile associato.
<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>
Ecco uno screenshot della pagina.
Modulo app
Tutte le richieste effettuate a Commerce Layer, diverse dall'autenticazione, devono contenere un token. Pertanto, nel momento in cui l'app viene inizializzata, viene recuperato un token dalla /oauth/token
sul server e viene inizializzata una sessione. Utilizzeremo il token APP_INITIALIZER
per fornire una funzione di inizializzazione in cui viene recuperato il token. Inoltre, utilizzeremo il token HTTP_INTERCEPTORS
per fornire OptionsInterceptor
che abbiamo creato in precedenza. Una volta aggiunti tutti i moduli, il file del modulo dell'app dovrebbe essere simile a questo.
@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 { }
Componente dell'app
Modificheremo il modello del componente dell'app e il suo stile che puoi trovare qui.
<div> <app-header></app-header> <div> <router-outlet></router-outlet> </div> </div>
Conclusione
In questo articolo, abbiamo spiegato come creare un'app di e-commerce Angular 11 con Commerce Layer e Paypal. Abbiamo anche toccato come strutturare l'app e come interfacciarsi con un'API di e-commerce.
Sebbene questa app consenta a un cliente di effettuare un ordine completo, non è affatto finita. C'è così tanto che potresti aggiungere per migliorarlo. Per uno, puoi scegliere di abilitare le modifiche alla quantità di articoli nel carrello, collegare gli articoli del carrello alle loro pagine di prodotto, ottimizzare i componenti dell'indirizzo, aggiungere ulteriori protezioni per le pagine di pagamento come la pagina di effettuazione dell'ordine e così via. Questo è solo il punto di partenza.
Se desideri saperne di più sul processo di creazione di un ordine dall'inizio alla fine, puoi consultare le guide e l'API di Commerce Layer. Puoi visualizzare il codice per questo progetto in questo repository.