Angular 11, Commerce Layer ve Paypal ile E-Ticaret Sitesi Nasıl Kurulur

Yayınlanan: 2022-03-10
Kısa özet ↬ Giderek daha fazla müşteri çevrimiçi alışverişe yöneldiğinden, bir e-ticaret mağazasına sahip olmak herhangi bir mağaza sahibi için çok önemlidir. Bu eğitimde, Angular 11 ile nasıl e-ticaret sitesi oluşturulacağını inceleyeceğiz. Site, Commerce Layer'ı başsız bir e-ticaret API'si olarak kullanacak ve ödemeleri işlemek için Paypal'ı kullanacak.

Günümüzde bir işletmeyi yönetirken çevrimiçi bir varlığa sahip olmak çok önemlidir. Önceki yıllara göre çok daha fazla çevrimiçi alışveriş yapılıyor. Bir e-ticaret mağazasına sahip olmak, mağaza sahiplerinin yalnızca bir tuğla ve harç mağazasıyla yararlanamayacakları diğer gelir akışlarını açmalarına olanak tanır. Ancak diğer mağaza sahipleri, işlerini fiziksel bir varlık olmadan tamamen çevrimiçi olarak yürütürler. Bu, bir çevrimiçi mağazaya sahip olmayı çok önemli hale getirir.

Etsy, Shopify ve Amazon gibi siteler, bir site geliştirme konusunda endişelenmenize gerek kalmadan oldukça hızlı bir şekilde mağaza kurmayı kolaylaştırır. Ancak, mağaza sahiplerinin kişiselleştirilmiş bir deneyim isteyebilecekleri veya bu platformlardan bazılarında mağaza sahibi olmanın maliyetinden tasarruf edebilecekleri durumlar olabilir.

Başsız e-ticaret API platformları, mağaza sitelerinin arayüz oluşturabileceği arka uçlar sağlar. Müşteri, sipariş, sevkiyat, ödeme vb. mağaza ile ilgili tüm süreçleri ve verileri yönetirler. Tek gereken, bu bilgilerle etkileşim kurmak için bir ön uç. Bu, müşterilerinin çevrimiçi mağazasını nasıl deneyimleyeceklerine ve onu nasıl işletmeyi seçeceklerine karar verme konusunda sahiplere çok fazla esneklik sağlar.

Bu yazımızda Angular 11 kullanarak e-ticaret mağazası nasıl kurulur anlatacağız. Headless e-ticaret API'miz olarak Commerce Layer kullanacağız. Ödemeleri işleme koymanın tonlarca yolu olsa da, sadece bir tanesinin, Paypal'ın nasıl kullanılacağını göstereceğiz.

  • GitHub'da kaynak kodunu görüntüleyin →

Önkoşullar

Uygulamayı oluşturmadan önce, Angular CLI'nin kurulu olması gerekir. Uygulamayı başlatmak ve iskele yapmak için kullanacağız. Henüz yüklemediyseniz, npm üzerinden alabilirsiniz.

 npm install -g @angular/cli

Ayrıca bir Ticaret Katmanı geliştirici hesabına ihtiyacınız olacak. Geliştirici hesabını kullanarak bir test organizasyonu oluşturmanız ve test verileriyle tohumlamanız gerekir. Tohumlama, hangi verileri kullanmanız gerekeceği konusunda endişelenmeden önce uygulamayı geliştirmeyi kolaylaştırır. Bu bağlantıdan bir hesap ve burada bir organizasyon oluşturabilirsiniz.

Ticaret Katmanı geliştirici hesabı kuruluşları panosu
Kuruluşunuzu eklediğiniz Ticaret Katmanı geliştirici hesabı kuruluşlar panosu. (Büyük önizleme)
Ticaret Katmanı kuruluşları oluşturma formu
Yeni bir organizasyon oluştururken Test verili Tohum kutusunu işaretleyin. (Büyük önizleme)

Son olarak, bir Paypal Sandbox hesabına ihtiyacınız olacak. Bu tür bir hesaba sahip olmak, gerçek parayı riske atmadan işletmeler ve kullanıcılar arasındaki işlemleri test etmemizi sağlayacaktır. Burada bir tane oluşturabilirsiniz. Korumalı alan hesabının bir test işletmesi vardır ve bunun için önceden oluşturulmuş bir test kişisel hesabı vardır.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Ticaret Katmanı ve Paypal Yapılandırması

Commerce Layer'da Paypal Sandbox ödemelerini mümkün kılmak için API anahtarlarını ayarlamanız gerekir. Paypal geliştirici hesabınızın hesaplara genel bakışına gidin. Bir işletme hesabı seçin ve hesap ayrıntılarının API kimlik bilgileri sekmesi altında, REST Apps altında Varsayılan Uygulamayı bulacaksınız.

Paypal Sandbox işletme hesabı ayrıntıları açılır penceresindeki API Kimlik Bilgileri sekmesi
Paypal işletme hesabı ayrıntıları açılır penceresinde varsayılan REST uygulamasını nerede bulabilirsiniz. (Büyük önizleme)
Paypal Sandbox işletme hesabı ayarlarında Varsayılan Uygulamaya genel bakış
REST API istemci kimliğini ve sırrını alabileceğiniz Paypal Sandbox işletme hesabı ayarlarında Varsayılan Uygulamaya genel bakış. (Büyük önizleme)

Paypal işletme hesabınızı Ticaret Katmanı kuruluşunuzla ilişkilendirmek için kuruluşunuzun kontrol paneline gidin. Burada, çeşitli pazarlarınız için bir Paypal ödeme ağ geçidi ve bir Paypal ödeme yöntemi ekleyeceksiniz. Ayarlar > Ödemeler altında, Ödeme Ağ Geçitleri > Paypal'ı seçin ve Paypal müşteri kimliğinizi ve sırrınızı ekleyin.

Ticaret Katmanında yeni Payments Gateway panosu
Paypal ödeme ağ geçidi oluşturmak için Ticaret Katmanı panosunda nerede. (Büyük önizleme)

Ağ geçidini oluşturduktan sonra, Paypal'ı bir seçenek olarak sunmak için hedeflediğiniz her pazar için bir Paypal ödeme yöntemi oluşturmanız gerekecektir. Bunu Ayarlar > Ödemeler > Ödeme Yöntemleri > Yeni Ödeme Yöntemi altında yapacaksınız.

Ticaret Katmanında Ödeme Yöntemleri panosu
Paypal ödeme yöntemi oluşturmak için Ticaret Katmanı panosunda nerede. (Büyük önizleme)

Kullanılan Rotalar Hakkında Bir Not

Ticaret Katmanı, kimlik doğrulama için bir yol ve API'leri için başka bir farklı yol kümesi sağlar. /oauth/token kimlik doğrulama yolları, bir belirteç için kimlik bilgilerini değiştirir. Bu belirteç, API'lerine erişmek için gereklidir. API yollarının geri kalanı, /api/:resource kalıbını alır.

Bu makalenin kapsamı, bu uygulamanın yalnızca ön uç bölümünü kapsar. Belirteçlerin sunucu tarafını saklamayı, sahipliği izlemek için oturumları kullanmayı ve istemciye bir oturum kimliğine sahip yalnızca http çerezleri sağlamayı seçtim. Bu makalenin kapsamı dışında olduğu için burada ele alınmayacaktır. Ancak, yollar aynı kalır ve tam olarak Ticaret Katmanı API'sine karşılık gelir. Bununla birlikte, kullanacağımız Ticaret Katmanı API'sinde bulunmayan birkaç özel yol vardır. Bunlar esas olarak oturum yönetimi ile ilgilenir. Onlara ulaştığımızda bunlara işaret edeceğim ve benzer bir sonucu nasıl elde edebileceğinizi anlatacağım.

Fark edebileceğiniz başka bir tutarsızlık, istek gövdelerinin Commerce Layer API'nin gerektirdiğinden farklı olmasıdır. İstekler bir belirteçle doldurulmak üzere başka bir sunucuya iletildiğinden, gövdeleri farklı şekilde yapılandırdım. Bu, istek göndermeyi kolaylaştırmak içindi. Talep organlarında herhangi bir tutarsızlık olduğunda, bunlar hizmetlerde belirtilecektir.

Bu kapsam dışında olduğundan, jetonları nasıl güvenli bir şekilde saklayacağınıza karar vermeniz gerekecek. Ayrıca, Commerce Layer API'nin gerektirdiğiyle tam olarak eşleşmesi için istek gövdelerini biraz değiştirmeniz gerekecektir. Bir tutarsızlık olduğunda, API referansına bağlantı vereceğim ve gövdenin nasıl doğru şekilde yapılandırılacağını ayrıntılı olarak açıklayacağım.

Uygulama Yapısı

Uygulamayı düzenlemek için dört ana bölüme ayıracağız. Modüllerin her birinin ne yaptığına ilişkin daha iyi bir açıklama, ilgili bölümlerin altında verilmiştir:

  1. çekirdek modül,
  2. veri modülü,
  3. paylaşılan modül,
  4. özellik modülleri.

Özellik modülleri, ilgili sayfaları ve bileşenleri birlikte gruplayacaktır. Dört özellik modülü olacaktır:

  1. yetkilendirme modülü,
  2. ürün modülü,
  3. sepet modülü,
  4. ödeme modülü.

Her modüle geldiğimizde amacının ne olduğunu açıklayacağım ve içeriğini parçalayacağım.

Aşağıda, src/app klasörünün bir ağacı ve her modülün bulunduğu yer yer almaktadır.

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

Uygulamayı Oluşturma ve Bağımlılıklar Ekleme

Uygulamayı oluşturarak başlayacağız. Kuruluşumuz Kireç Markası olarak adlandırılacak ve Ticaret Katmanı tarafından önceden oluşturulmuş test verilerine sahip olacaktır.

 ng new lime-app

Birkaç bağımlılığa ihtiyacımız olacak. Ağırlıklı olarak Açısal Malzeme ve Yok Edilene Kadar. Angular Material, bileşenler ve stil sağlayacaktır. Destroy, bileşenler yok edildiğinde otomatik olarak gözlemlenebilirlerin aboneliğini iptal edene kadar. Bunları yüklemek için şunu çalıştırın:

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

Varlıklar

Commerce Layer'a adres eklerken bir alfa-2 ülke kodunun kullanılması gerekir. Bu kodları içeren bir json dosyası assets/json/country-codes.json adresindeki assets klasörüne ekleyeceğiz. Bu dosyayı burada bağlantılı olarak bulabilirsiniz.

stiller

Yaratacağımız bileşenler bazı küresel stilleri paylaşıyor. Bunları bu linkte bulunabilecek styles.css içine yerleştireceğiz.

Çevre

Yapılandırmamız iki alandan oluşacaktır. Ticaret Katmanı API'sini göstermesi gereken apiUrl . apiUrl , verileri getirmek için oluşturacağımız hizmetler tarafından kullanılır. clientUrl , uygulamanın üzerinde çalıştığı etki alanı olmalıdır. Paypal için yönlendirme URL'lerini ayarlarken bunu kullanırız. Bu dosyaya bu linkten ulaşabilirsiniz.

Paylaşılan Modül

Paylaşılan modül, diğer modüller arasında paylaşılan hizmetleri, boruları ve bileşenleri içerecektir.

 ng gm shared

Üç bileşenden, bir borudan ve iki hizmetten oluşur. İşte böyle görünecek.

 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

Ayrıca, yaygın olarak kullanılan bazı Angular Material bileşenlerini dışa aktarmak için paylaşılan modülü kullanacağız. Bu, her bileşeni çeşitli modüller arasında içe aktarmak yerine kutudan çıkarmayı kolaylaştırır. İşte shared.module.ts içereceği şeyler.

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

Bileşenler

Ürün Miktarı Bileşeni

Bu bileşen, sepete eklenirken öğelerin miktarını belirler. Sepet ve ürünler modüllerinde kullanılacaktır. Bir malzeme seçici bu amaç için kolay bir seçim olurdu. Ancak, malzeme seçiminin stili, diğer tüm formlarda kullanılan malzeme girdileriyle eşleşmedi. Malzeme menüsü, kullanılan malzeme girişlerine çok benziyordu. Bunun yerine onunla bir seçme bileşeni oluşturmaya karar verdim.

 ng gc shared/components/item-quantity

Bileşenin üç girdi özelliği ve bir çıktı özelliği olacaktır. quantity öğelerin ilk miktarını ayarlar, maxValue tek seferde seçilebilecek maksimum öğe sayısını belirtir ve disabled , bileşenin devre dışı bırakılıp bırakılmaması gerektiğini belirtir. Bir miktar seçildiğinde setQuantityEvent tetiklenir.

Bileşen başlatıldığında, malzeme menüsünde görünen değerleri ayarlayacağız. setQuantity olaylarını yayan setQuantityEvent adında bir yöntem de vardır.

Bu bileşen dosyasıdır.

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

Bu onun şablonu.

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

İşte onun stili.

 button { margin: 3px; }

Başlık Bileşeni

Bu bileşen, bazı daha basit sayfalarda düz bir başlık olduğu kadar bir adım başlığı olarak da kullanılabilir. Angular Material kademeli bir bileşen sağlasa da, oldukça uzun bir ödeme süreci için en uygunu değildi, daha küçük ekranlarda o kadar duyarlı değildi ve uygulanması çok daha fazla zaman gerektiriyordu. Ancak daha basit bir başlık, kademeli bir gösterge olarak yeniden kullanılabilir ve birden çok sayfada faydalı olabilir.

 ng gc shared/components/title

Bileşenin dört giriş özelliği vardır: title , subtitle başlık , sayı ( no ) ve bileşen metninin ortalanıp ortalanmayacağını belirtmek için centerText .

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

Aşağıda onun şablonu. Stilini burada bağlantılı olarak bulabilirsiniz.

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

Basit Sayfa Bileşeni

Bir sayfa için gereken tek şeyin bir başlık, bir simge ve bir düğme olduğu birden çok örnek vardır. Bunlara 404 sayfası, boş sepet sayfası, hata sayfası, ödeme sayfası ve sipariş verme sayfası dahildir. Basit sayfa bileşeninin hizmet edeceği amaç budur. Sayfadaki düğme tıklandığında, ya bir rotaya yönlendirilecek ya da bir buttonEvent yanıt olarak bazı eylemler gerçekleştirecektir.

Onu yapmak için:

 ng gc shared/components/simple-page

Bu onun bileşen dosyasıdır.

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

Ve şablonu şunları içerir:

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

Bu stil burada bulunabilir.

borular

Kelime Sarma Borusu

Sitede görüntülenen bazı ürünlerin adları ve diğer bilgi türleri gerçekten uzun. Bazı durumlarda, bu uzun cümleleri maddi bileşenlere sığdırmak zordur. Bu yüzden, cümleleri belirli bir uzunluğa indirmek ve sonucun sonuna elips eklemek için bu boruyu kullanacağız.

Oluşturmak için çalıştırın:

 ng g pipe shared/pipes/word-wrap

Şunları içerecektir:

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

Hizmetler

HTTP Hata İşleyici Hizmeti

Bu projede oldukça fazla sayıda http servisi bulunmaktadır. Her yöntem için bir hata işleyicisi oluşturmak tekrarlayıcıdır. Bu nedenle, tüm yöntemlerle kullanılabilecek tek bir işleyici oluşturmak mantıklıdır. Hata işleyici, bir hatayı biçimlendirmek ve hataları diğer harici günlük platformlarına iletmek için kullanılabilir.

Çalıştırarak oluşturun:

 ng gs shared/services/http-error-handler

Bu hizmet yalnızca bir yöntem içerecektir. Yöntem, bir istemci veya sunucu hatası olmasına bağlı olarak görüntülenecek hata mesajını biçimlendirir. Ancak, daha da geliştirmek için yer var.

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

Yerel Depolama Hizmeti

Bir alışveriş sepetindeki öğelerin sayısını takip etmek için yerel depolamayı kullanacağız. Bir siparişin kimliğini burada saklamak da yararlıdır. Bir sipariş, Ticaret Katmanındaki bir sepete karşılık gelir.

Yerel depolama hizmeti çalıştırmak için:

 ng gs shared/services/local-storage

Hizmet, yerel depolamadan öğe eklemek, silmek ve almak için dört yöntem ve onu temizlemek için başka yöntemler içerir.

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

Veri Modülü

Bu modül, veri alma ve yönetiminden sorumludur. Uygulamamızın tükettiği verileri almak için kullanacağımız şey budur. Yapısı aşağıdadır:

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

Modül çalıştırmasını oluşturmak için:

 ng gm data

Modeller

Modeller, API'den tükettiğimiz verilerin nasıl yapılandırıldığını tanımlar. 16 arayüz bildirimimiz olacak. Bunları oluşturmak için çalıştırın:

 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

Aşağıdaki tablo her dosyaya bağlantı verir ve her bir arabirimin ne olduğuna ilişkin bir açıklama verir.

Arayüz Tanım
Adres Genel bir adresi temsil eder.
Araba Müşterinin satın almayı planladığı ürün sayısını takip eden bir siparişin müşteri tarafı versiyonu.
Ülke Alfa-2 ülke kodu.
Müşteri adresi Bir müşteriyle ilişkili bir adres.
Müşteri Kayıtlı bir kullanıcı.
Teslimat Süresi Bir gönderinin teslim edilmesi için gereken süreyi temsil eder.
Satır Öğesi Sepete eklenen ayrıntılı bir ürün.
Sipariş Bir alışveriş sepeti veya satır öğeleri koleksiyonu.
Ödeme şekli Bir sipariş için kullanıma sunulan bir ödeme türü.
Ödeme Kaynağı Bir siparişle ilişkili ödeme.
PayPal ödemesi Paypal üzerinden yapılan bir ödeme
Fiyat Bir SKU ile ilişkili fiyat.
gönderi Birlikte gönderilen öğelerin toplanması.
Nakliye Yöntemi Bir paketin sevk edildiği yöntem.
SKU Benzersiz bir stok tutma birimi.
Stok Konumu SKU envanterini içeren konum.

Hizmetler

Bu klasör, uygulama verilerini oluşturan, alan ve işleyen hizmetleri içerir. Burada 11 servis oluşturacağız.

 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

Adres Hizmeti

Bu hizmet adresleri oluşturur ve alır. Siparişlere gönderim ve fatura adresleri oluştururken ve atarken önemlidir. İki yöntemi vardır. Biri bir adres oluşturmak için, diğeri bir tane almak için.

Burada kullanılan rota /api/addresses . Doğrudan Ticaret Katmanı API'sini kullanacaksanız, verileri bu örnekte gösterildiği gibi yapılandırdığınızdan emin olun.

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

Sepet Hizmeti

Sepet, eklenen öğelerin miktarını ve sipariş kimliğini korumaktan sorumludur. Her yeni satır öğesi oluşturulduğunda bir siparişteki öğelerin sayısını almak için API çağrıları yapmak pahalı olabilir. Bunun yerine, istemcinin sayısını korumak için yerel depolamayı kullanabiliriz. Bu, sepete her ürün eklendiğinde gereksiz sipariş alımları yapma ihtiyacını ortadan kaldırır.

Bu hizmeti ayrıca sipariş kimliğini saklamak için kullanırız. Sepet, Ticaret Katmanındaki bir siparişe karşılık gelir. Sepete ilk ürün eklendikten sonra sipariş oluşturulur. Ödeme işlemi sırasında getirebilmemiz için bu sipariş kimliğini korumamız gerekiyor.

Ek olarak, sepete bir öğenin eklendiğini başlığa iletmek için bir yola ihtiyacımız var. Başlık, sepet düğmesini içerir ve içindeki öğelerin miktarını görüntüler. Sepetin geçerli değeriyle bir BehaviorSubject gözlemlenebilirini kullanacağız. Başlık buna abone olabilir ve alışveriş sepeti değerindeki değişiklikleri izleyebilir.

Son olarak, bir sipariş tamamlandıktan sonra sepet değerinin silinmesi gerekir. Bu, sonraki yeni siparişleri oluştururken herhangi bir karışıklık olmamasını sağlar. Mevcut sipariş verildi olarak işaretlendiğinde, saklanan değerler silinir.

Tüm bunları daha önce oluşturulan yerel depolama hizmetini kullanarak gerçekleştireceğiz.

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

Ülke Hizmeti

Ticaret Katmanına adres eklerken, ülke kodunun alfa 2 kodu olması gerekir. Bu hizmet, her ülke için bu kodları içeren bir json dosyasını okur ve onu getCountries yönteminde döndürür.

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

Müşteri Adresi Hizmeti

Bu hizmet, adresleri müşterilerle ilişkilendirmek için kullanılır. Ayrıca, bir müşteriyle ilgili belirli bir adresi veya tüm adresleri getirir. Müşteri, teslimat ve fatura adreslerini siparişine eklediğinde kullanılır. createCustomer yöntemi bir müşteri oluşturur, getCustomerAddresses bir müşterinin tüm adreslerini alır ve getCustomerAddress belirli bir adresi alır.

Bir müşteri adresi oluştururken, posta gövdesini bu örneğe göre yapılandırdığınızdan emin olun.

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

Müşteri servisi

Bu hizmet kullanılarak müşteriler oluşturulur ve bilgileri alınır. Bir kullanıcı kaydolduğunda müşteri olur ve createCustomerMethod kullanılarak oluşturulur. getCustomer , belirli bir Id ile ilişkili müşteriyi döndürür. getCurrentCustomer , oturum açmış olan müşteriyi döndürür.

Müşteri oluştururken verileri bu şekilde yapılandırın. Adlarını ve soyadlarını, özniteliklerinde gösterildiği gibi meta verilere ekleyebilirsiniz.

/api/customers/current yolu, Commerce Layer'da mevcut değil. Bu nedenle, şu anda oturum açmış olan müşteriyi nasıl alacağınızı bulmanız gerekecek.

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

Teslimat Süresi Hizmeti

Bu hizmet, çeşitli stok konumlarından sevkiyat zaman çizelgeleri hakkında bilgi verir.

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

Satır Öğesi Hizmeti

Sepete eklenen ürünler bu servis tarafından yönetilir. Bununla, sepete eklendiği anda bir ürün oluşturabilirsiniz. Bir öğenin bilgileri de alınabilir. Ürün, miktarı değiştiğinde güncellenebilir veya sepetten çıkarıldığında silinebilir.

Öğeleri oluştururken veya güncellerken, istek gövdesini bu örnekte gösterildiği gibi yapılandırın.

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

Sipariş Hizmeti

Satır öğesi hizmetine benzer şekilde, sipariş hizmeti de bir sipariş oluşturmanıza, güncellemenize, silmenize veya almanıza olanak tanır. Ayrıca, bir siparişle ilişkili gönderileri getOrderShipments yöntemini kullanarak ayrı olarak almayı da seçebilirsiniz. Bu hizmet, ödeme işlemi boyunca yoğun bir şekilde kullanılır.

Ödeme sırasında gerekli olan bir siparişle ilgili farklı türde bilgiler vardır. Bütün bir siparişi ve onun ilişkilerini getirmek pahalı olabileceğinden, bir siparişten ne almak istediğimizi GetOrderParams kullanarak belirtiriz. Bunun CL API'sindeki karşılığı, dahil edilecek sipariş ilişkilerini listelediğiniz içerme sorgu parametresidir. Sepet özeti için hangi alanların eklenmesi gerektiğini buradan ve çeşitli ödeme aşamaları için buradan kontrol edebilirsiniz.

Aynı şekilde, bir siparişi güncellerken güncelleme alanlarını belirtmek için UpdateOrderParams kullanırız. Bunun nedeni, jetonu dolduran sunucuda, hangi alanın güncellendiğine bağlı olarak bazı ekstra işlemlerin yapılmasıdır. Ancak, CL API'ye doğrudan istek yapıyorsanız, bunu belirtmeniz gerekmez. CL API bunları belirtmenizi gerektirmediğinden, bunu ortadan kaldırabilirsiniz. Bununla birlikte, istek gövdesi bu örneğe benzemelidir.

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

Paypal Ödeme Hizmeti

Bu hizmet, siparişler için Paypal ödemelerinin oluşturulmasından ve güncellenmesinden sorumludur. Ek olarak, kimliği verilen bir Paypal ödemesi alabiliriz. Paypal ödemesi oluştururken posta gövdesi bu örneğe benzer bir yapıya sahip olmalıdır.

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

Sevkiyat Hizmeti

Bu hizmet, bir gönderi alır veya kimliğini verdiği güncellemeleri alır. Bir gönderi güncellemesinin istek gövdesi bu örneğe benzer görünmelidir.

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

SKU Hizmeti

SKU hizmeti, ürünleri mağazadan alır. Birden fazla ürün alınıyorsa, sayfalara ayrılabilir ve bir sayfa boyutu ayarlanabilir. API'ye doğrudan istek yapıyorsanız, bu örnekte olduğu gibi sayfa boyutu ve sayfa numarası sorgu parametreleri olarak ayarlanmalıdır. Kimliği verilen tek bir ürün de alınabilir.

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

Çekirdek modülü

Çekirdek modül, uygulamada merkezi ve ortak olan her şeyi içerir. Bunlar, başlık gibi bileşenleri ve 404 sayfası gibi sayfaları içerir. Kimlik doğrulama ve oturum yönetiminden sorumlu hizmetlerin yanı sıra uygulama genelindeki önleyiciler ve korumalar da burada yer alır.

Çekirdek modül ağacı böyle görünecek.

 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

Modülü oluşturmak ve içeriğini çalıştırmak için:

 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

Çekirdek modül dosyası bunu beğenmeli. NotFoundComponent ve ErrorComponent için rotaların kaydedildiğini unutmayın.

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

Hizmetler

Hizmetler klasörü, kimlik doğrulama, oturum ve başlık hizmetlerini içerir.

Kimlik Doğrulama Hizmeti

AuthenticationService , istemci ve müşteri belirteçleri almanıza olanak tanır. Bu belirteçler, API'nin geri kalan yollarına erişmek için kullanılır. Bir kullanıcı bunun için bir e-posta ve parola değiştirdiğinde ve daha geniş bir izin yelpazesine sahip olduğunda, müşteri belirteçleri iade edilir. İstemci belirteçleri, kimlik bilgilerine ihtiyaç duymadan verilir ve daha dar izinlere sahiptir.

getClientSession bir istemci belirteci alır. login bir müşteri jetonu alır. Her iki yöntem de bir oturum oluşturur. İstemci belirteci isteğinin gövdesi şöyle görünmelidir ve müşteri belirteci böyle görünmelidir.

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

Bileşenler

Error Component

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

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

This is what the component will look like.

Screenshot of error page
Screenshot of error page. (Büyük önizleme)

Not Found Component

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

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

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.

Bir kullanıcının oturumunu yok eden ve onlara bir istemci belirteci atayan bir logout yöntemi vardır. Müşteri belirtecini koruyan oturum yok edildiğinden ve her API isteği için hala bir belirteç gerektiğinden bir istemci belirteci atanır. Bir materyal snackbar, kullanıcıya oturumlarının başarıyla yok edilip edilmediğini bildirir.

Bileşen yok edildiğinde tüm aboneliklerin otomatik olarak iptal edilmesi gerektiğini belirtmek için @UntilDestroy({ checkProperties: true }) dekoratörünü kullanırız.

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

Aşağıda başlık şablonu yer almaktadır ve burada stiliyle bağlantılıdır.

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

Muhafızlar

Boş Araba Bekçisi

Bu koruma, sepetleri boşsa, kullanıcıların ödeme ve faturalandırmayla ilgili yollara erişmesini engeller. Bunun nedeni, ödeme işlemine devam etmek için geçerli bir sipariş olması gerektiğidir. Bir sipariş, içinde öğeler bulunan bir sepete karşılık gelir. Sepette öğeler varsa, kullanıcı korumalı bir sayfaya geçebilir. Ancak, sepet boşsa, kullanıcı boş bir sepet sayfasına yönlendirilir.

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

önleyiciler

Seçenekler Durdurucu

Bu önleyici, tüm giden HTTP isteklerini durdurur ve isteğe iki seçenek ekler. Bunlar, bir Content-Type üst bilgisi ve bir withCredentials özelliğidir. withCredentials , bir isteğin, kullandığımız yalnızca http tanımlama bilgileri gibi giden kimlik bilgileriyle gönderilip gönderilmeyeceğini belirtir. Sunucuya json kaynakları gönderdiğimizi belirtmek için Content-Type kullanıyoruz.

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

Özellik Modülleri

Bu bölüm, uygulamanın ana özelliklerini içerir. Daha önce belirtildiği gibi, özellikler dört modülde gruplandırılmıştır: auth, product, cart ve checkout modülleri.

Ürünler Modülü

Ürünler modülü, indirimdeki ürünleri gösteren sayfalar içerir. Bunlara ürün sayfası ve ürün listesi sayfası dahildir. Aşağıda gösterildiği gibi yapılandırılmıştır.

 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

Onu ve bileşenlerini oluşturmak için:

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

Bu modül dosyasıdır:

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

Ürün Listesi Bileşeni

Bu bileşen, satılık mevcut ürünlerin sayfalandırılmış bir listesini görüntüler. Uygulama başladığında yüklenen ilk sayfadır.

Ürünler bir ızgarada görüntülenir. Bunun için en iyi bileşen malzeme ızgara listesidir. Izgarayı duyarlı hale getirmek için ızgara sütunlarının sayısı ekran boyutuna bağlı olarak değişecektir. BreakpointObserver hizmeti, ekranın boyutunu belirlememize ve başlatma sırasında sütunları atamamıza izin verir.

Ürünleri almak için SkuService getProducts çağırıyoruz. Başarılı ise ürünleri geri döner ve grid'e atar. Değilse, kullanıcıyı hata sayfasına yönlendiririz.

Görüntülenen ürünler sayfalandırılmış olduğundan, ek ürünleri almak için bir getNextPage yöntemimiz olacak.

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

Şablon aşağıda gösterilmiştir ve stili burada bulunabilir.

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

Sayfa böyle görünecek.

Ürün listesi sayfasının ekran görüntüsü
Ürün listesi sayfasının ekran görüntüsü. (Büyük önizleme)

Ürün Bileşeni

Ürün listesi sayfasından bir ürün seçildiğinde, bu bileşen ayrıntılarını görüntüler. Bunlar, ürünün tam adını, fiyatını ve açıklamasını içerir. Ürünü sepete eklemek için bir düğme de vardır.

Başlatmada, route parametrelerinden ürünün kimliğini alırız. Kimliği kullanarak ürünü SkuService .

Kullanıcı sepete bir ürün eklediğinde, addItemToCart yöntemi çağrılır. İçinde, sepet için önceden bir sipariş oluşturulup oluşturulmadığını kontrol ederiz. Değilse, OrderService kullanılarak yenisi yapılır. Ardından, ürüne karşılık gelen siparişte bir satır öğesi oluşturulur. Sepet için zaten bir sipariş varsa, yalnızca satır öğesi oluşturulur. Taleplerin durumuna göre kullanıcıya snackbar mesajı gösterilir.

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

ProductComponent şablonu aşağıdaki gibidir ve stili burada bağlantılıdır.

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

Sayfa böyle görünecek.

Ürün sayfasının ekran görüntüsü
Ürün sayfasının ekran görüntüsü. (Büyük önizleme)

Yetkilendirme Modülü

Auth modülü, kimlik doğrulamadan sorumlu sayfaları içerir. Bunlar, giriş ve kayıt sayfalarını içerir. Aşağıdaki gibi yapılandırılmıştır.

 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

Onu ve bileşenlerini oluşturmak için:

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

Bu onun modül dosyasıdır.

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

Kayıt Bileşeni

Bir kullanıcı bu bileşeni kullanarak bir hesaba kaydolur. İşlem için bir ad, soyad, e-posta ve şifre gereklidir. Kullanıcının ayrıca şifresini onaylaması gerekir. Giriş alanları FormBuilder hizmeti ile oluşturulacaktır. Doğrulama, tüm girişlerin değerlere sahip olmasını zorunlu kılmak için eklenir. Minimum sekiz karakter uzunluğundan emin olmak için parola alanına ek doğrulama eklenir. Özel bir matchPasswords doğrulayıcı, onaylanan parolanın ilk parolayla eşleşmesini sağlar.

Bileşen başlatıldığında, başlıktaki sepet, oturum açma ve oturum kapatma düğmeleri gizlenir. Bu, HeaderService kullanılarak başlığa iletilir.

Tüm alanlar geçerli olarak işaretlendikten sonra kullanıcı kaydolabilir. signup yönteminde, CustomerService createCustomer yöntemi bu girişi alır. Kayıt başarılı olursa, kullanıcı hesabının bir snackbar kullanılarak başarıyla oluşturulduğu konusunda bilgilendirilir. Daha sonra ana sayfaya yönlendirilirler.

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

SignupComponent için şablon aşağıdadır.

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

Bileşen aşağıdaki gibi ortaya çıkacaktır.

Kayıt sayfasının ekran görüntüsü
Kayıt sayfasının ekran görüntüsü. (Büyük önizleme)

Giriş Bileşeni

Kayıtlı bir kullanıcı bu bileşenle hesabına giriş yapar. Bir e-posta ve şifre girilmelidir. Karşılık gelen giriş alanları, onları gerekli kılan doğrulamaya sahip olacaktır.

SignupComponent benzer şekilde, başlıktaki sepet, oturum açma ve oturum kapatma düğmeleri gizlenir. Görünürlükleri, bileşen başlatma sırasında HeaderService kullanılarak ayarlanır.

Oturum açmak için kimlik bilgileri AuthenticationService iletilir. Başarılı olursa, kullanıcının oturum açma durumu SessionService kullanılarak ayarlanır. Kullanıcı daha sonra bulundukları sayfaya geri yönlendirilir. Başarısız olursa, hata içeren bir snackbar görüntülenir ve şifre alanı sıfırlanır.

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

LoginComponent şablonu aşağıdadır.

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

İşte sayfanın bir ekran görüntüsü.

Giriş sayfasının ekran görüntüsü
Giriş sayfasının ekran görüntüsü. (Büyük önizleme)

Sepet Modülü

Sepet modülü, sepetle ilgili tüm sayfaları içerir. Bunlara sipariş özeti sayfası, kupon ve hediye kartı kodu sayfası ve boş bir sepet sayfası dahildir. Aşağıdaki gibi yapılandırılmıştır.

 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

Oluşturmak için şunu çalıştırın:

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

Bu modül dosyasıdır.

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

Kodlar Bileşen

Daha önce belirtildiği gibi, bu bileşen bir siparişe herhangi bir kupon veya hediye kartı kodu eklemek için kullanılır. Bu, kullanıcının ödeme işlemine geçmeden önce siparişlerinin toplamına indirim uygulamasına olanak tanır.

İki giriş alanı olacaktır. Biri kuponlar için, diğeri hediye kartı kodları için.

Kodlar, sipariş güncellenerek eklenir. updateOrder yöntemi, siparişi OrderService günceller. Daha sonra her iki alan da sıfırlanır ve kullanıcı bir snackbar ile işlemin başarısı hakkında bilgilendirilir. Bir hata oluştuğunda bir snackbar da gösterilir. Hem addCoupon hem de addGiftCard yöntemleri updateOrder yöntemini çağırır.

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

Şablon aşağıda gösterilmiştir ve stili bu bağlantıda bulunabilir.

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

İşte sayfanın bir ekran görüntüsü.

Kodlar sayfasının ekran görüntüsü
Kodlar sayfasının ekran görüntüsü. (Büyük önizleme)

Boş Bileşen

Boş bir araba ile kontrol etmek mümkün olmamalıdır. Kullanıcıların boş sepetlerle ödeme modülü sayfalarına erişmesini engelleyen bir koruma olması gerekir. Bu zaten CoreModule kapsamında ele alınmıştır. Koruma, boş sepetli ödeme sayfalarına yönelik istekleri EmptyCartComponent yönlendirir.

Kullanıcıya sepetinin boş olduğunu belirten bir metin içeren çok basit bir bileşendir. Ayrıca, kullanıcının sepete bir şeyler eklemek için ana sayfaya gitmek için tıklayabileceği bir düğme vardır. Bu yüzden onu görüntülemek için SimplePageComponent kullanacağız. İşte şablon.

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

İşte sayfanın bir ekran görüntüsü.

Boş sepet sayfasının ekran görüntüsü
Boş sepet sayfasının ekran görüntüsü. (Büyük önizleme)

Özet Bileşeni

Bu bileşen, alışveriş sepetini/siparişi özetler. Sepetteki tüm ürünleri, adlarını, miktarlarını ve resimlerini listeler. Ayrıca vergiler, nakliye ve indirimler dahil olmak üzere siparişin maliyetini de düşürür. Kullanıcı bunu görüntüleyebilmeli ve ödemeye geçmeden önce öğelerden ve maliyetten memnun olup olmadığına karar verebilmelidir.

Başlatmada, sipariş ve satır öğeleri OrderService kullanılarak getirilir. Kullanıcı, satır öğelerini değiştirebilmeli ve hatta bunları siparişten kaldırabilmelidir. deleteLineItem yöntemi çağrıldığında öğeler kaldırılır. İçinde LineItemService deleteLineItem yöntemi, silinecek satır öğesinin kimliğini alır. Bir silme işlemi başarılı olursa, CartService kullanarak sepetteki ürün sayısını güncelleriz.

Daha sonra kullanıcı, kontrol etme işlemine başladıkları müşteri sayfasına yönlendirilir. checkout yöntemi yönlendirmeyi yapar.

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

Aşağıda şablon ve stili burada bağlantılıdır.

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

İşte sayfanın bir ekran görüntüsü.

Özet sayfasının ekran görüntüsü
Özet sayfasının ekran görüntüsü. (Büyük önizleme)

Ödeme Modülü

Bu modül, ödeme işleminden sorumludur. Ödeme, bir fatura ve gönderim adresi, bir müşteri e-postası sağlamayı ve bir gönderim ve ödeme yöntemi seçmeyi içerir. Bu sürecin son adımı siparişin verilmesi ve onaylanmasıdır. Modülün yapısı aşağıdaki gibidir.

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

Bu modül açık ara en büyüğüdür ve 3 bileşen ve 7 sayfa içerir. Oluşturmak ve bileşenlerini çalıştırmak için:

 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

Bu modül dosyasıdır.

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

Bileşenler

Ülke Seçim Bileşeni

Bu bileşen, bir kullanıcının bir adresin parçası olarak bir ülke seçmesini sağlar. Malzeme seçme bileşeni, adres formundaki giriş alanları ile karşılaştırıldığında oldukça farklı bir görünüme sahiptir. Bu nedenle, tekdüzelik adına, bunun yerine bir malzeme menüsü bileşeni kullanılır.

Bileşen başlatıldığında, ülke kodu verileri CountryService kullanılarak alınır. countries özelliği, hizmet tarafından döndürülen değerleri tutar. Bu değerler şablondaki menüye eklenecektir.

Bileşenin bir çıktı özelliği vardır, setCountryEvent . Bir ülke seçildiğinde, bu olay ülkenin alfa-2 kodunu yayar.

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

Aşağıda şablonu ve burada bağlantılı olan stilidir.

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

Adres Bileşeni

Bu, adresleri yakalamak için bir formdur. Hem sevkiyat hem de fatura adresi sayfaları tarafından kullanılır. Geçerli bir Ticaret Katmanı adresi, ad ve soyadı, adres satırı, şehir, posta kodu, eyalet kodu, ülke kodu ve telefon numarası içermelidir.

FormBuilder hizmeti, form grubunu oluşturacaktır. Bu bileşen birden çok sayfa tarafından kullanıldığından, bir dizi girdi ve çıktı özelliğine sahiptir. Giriş özellikleri, düğme metnini, görüntülenen başlığı ve bir onay kutusu metnini içerir. Çıkış özellikleri, adresi oluşturmak için düğmeye tıklandığında ve onay kutusu değeri değiştiğinde başka bir olay yayıcıları olacaktır.

Butona tıklandığında addAddress metodu çağrılır ve createAddress olayı tam adresi yayar. Benzer şekilde, onay kutusu işaretlendiğinde, isCheckboxChecked olayı, onay kutusu değerini yayar.

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

İşte şablonu. Stilini burada bulabilirsiniz.

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

Sayfalar

Müşteri Bileşeni

Bir siparişin bir e-posta adresiyle ilişkilendirilmesi gerekir. Bu bileşen, müşteri e-posta adresini yakalayan bir formdur. Bileşen başlatıldığında, oturum açmışsa mevcut müşterinin e-posta adresi alınır. CustomerService . E-posta adreslerini değiştirmek istemezlerse, bu e-posta varsayılan değer olacaktır.

E-posta değiştirilirse veya bir müşteri giriş yapmazsa, sipariş girilen e-posta ile güncellenir. Siparişi yeni e-posta adresiyle güncellemek için OrderService kullanıyoruz. Başarılı olursa müşteriyi fatura adresi sayfasına yönlendiririz.

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

İşte bileşen şablonu ve burada bağlantılı stili.

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

İşte müşteri sayfasının bir ekran görüntüsü.

Müşteri sayfasının ekran görüntüsü
Müşteri sayfasının ekran görüntüsü. (Büyük önizleme)

Fatura Adresi Bileşeni

Fatura adresi bileşeni, bir müşterinin yeni bir fatura adresi eklemesine veya mevcut adreslerinden seçim yapmasına olanak tanır. Oturum açmamış kullanıcılar yeni bir adres girmek zorundadır. Giriş yapmış olanlar, yeni veya mevcut adresler arasında seçim yapma seçeneğine sahiptir.

showAddress özelliği, bileşende mevcut adreslerin gösterilip gösterilmeyeceğini belirtir. sameShippingAddressAsBilling , sevkıyat adresinin ayarlanan fatura adresiyle aynı olması gerekip gerekmediğini belirtir. Bir müşteri mevcut bir adresi seçtiğinde, kimliği selectedCustomerAddressId öğesine atanır.

Bileşen başlatıldığında, mevcut kullanıcının oturum açıp açmadığını kontrol etmek için SessionService kullanırız. Eğer oturum açtılarsa, varsa mevcut adresleri görüntüleyeceğiz.

Daha önce belirtildiği gibi, bir kullanıcı oturum açtıysa, fatura adresi olarak mevcut bir adresi seçebilir. updateBillingAddress yönteminde, giriş yapmışlarsa seçtikleri adres klonlanır ve siparişin fatura adresi olarak ayarlanır. Bunu, updateOrder yöntemini kullanarak siparişi güncelleyerek ve adres OrderService sağlayarak yapıyoruz.

Oturum açmamışlarsa, kullanıcının bir adres vermesi gerekir. Sağlandıktan sonra adres, createAddress yöntemi kullanılarak oluşturulur. İçinde, AddressService girişi alır ve yeni adresi yapar. Bundan sonra, yeni oluşturulan adresin kimliği kullanılarak sipariş güncellenir. Bir hata varsa veya herhangi bir işlem başarılıysa, bir snackbar gösteririz.

Gönderim adresi olarak aynı adres seçilirse, kullanıcı gönderi yöntemleri sayfasına yönlendirilir. Alternatif bir gönderim adresi sağlamak isterlerse, gönderim adresi sayfasına yönlendirilirler.

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

İşte şablon. Bu bağlantı, stiline işaret ediyor.

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

Fatura adresi sayfası böyle görünecek.

Fatura adresi sayfasının ekran görüntüsü
Fatura adresi sayfasının ekran görüntüsü. (Büyük önizleme)

Sevkiyat Adresi Bileşeni

Sevkiyat adresi bileşeni, fatura adresi bileşeni gibi davranır. Ancak, birkaç farklılık var. Birincisi, şablonda görüntülenen metin farklıdır. Diğer önemli farklılıklar, bir adres oluşturulduktan veya seçildikten sonra OrderService kullanılarak siparişin nasıl güncellendiğidir. Siparişin güncellendiği alanlar, seçilen adresler için shippingAddressCloneId ve yeni adresler için shippingAddress alanlarıdır. Bir kullanıcı fatura adresini teslimat adresiyle aynı olacak şekilde değiştirmeyi seçerse, billingAddressSameAsShipping alanı güncellenir.

Bir sevkiyat adresi seçildikten ve sipariş güncellendikten sonra, kullanıcı sevkiyat yöntemleri sayfasına yönlendirilir.

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

İşte şablon ve stili burada bulunabilir.

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

Gönderim adresi sayfası aşağıdaki gibi görünecektir.

Gönderim adresi sayfasının ekran görüntüsü
Gönderim adresi sayfasının ekran görüntüsü. (Büyük önizleme)

Nakliye Yöntemleri Bileşeni

Bu bileşen, bir siparişin yerine getirilmesi için gereken gönderi sayısını, mevcut nakliye yöntemlerini ve bunlarla ilişkili maliyetleri görüntüler. Müşteri daha sonra her gönderi için tercih ettiği bir nakliye yöntemini seçebilir.

shipments özelliği, siparişin tüm gönderilerini içerir. shipmentsForm Formu, gönderim yöntemi seçimlerinin yapılacağı formdur.

Bileşen başlatıldığında, sipariş alınır ve hem satır öğelerini hem de gönderilerini içerecektir. Aynı zamanda, çeşitli nakliye yöntemleri için teslimat süreleri alıyoruz. Siparişi almak için OrderService ve teslim süreleri için DeliveryLeadTimeService kullanırız. Her iki bilgi grubu da iade edildiğinde, bir dizi gönderide birleştirilir ve shipments özelliğine atanır. Her gönderi, öğelerini, mevcut nakliye yöntemlerini ve ilgili maliyeti içerecektir.

Kullanıcı her gönderi için bir gönderi yöntemi seçtikten sonra, seçilen gönderi yöntemi setShipmentMethods içinde her biri için güncellenir. Başarılı olursa, kullanıcı ödeme sayfasına yönlendirilir.

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

İşte şablon ve stili bu bağlantıda bulabilirsiniz.

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

Bu, gönderim yöntemleri sayfasının ekran görüntüsüdür.

Gönderim yöntemleri sayfasının ekran görüntüsü
Gönderim yöntemleri sayfasının ekran görüntüsü. (Büyük önizleme)

Ödemeler Bileşeni

Bu bileşende, kullanıcı siparişinin ödemesini Paypal ile yapmak istiyorsa ödeme düğmesine tıklar. approvalUrl , kullanıcının düğmeye tıkladığında yönlendirildiği Paypal bağlantısıdır.

Başlatma sırasında, OrderService kullanılarak dahil edilen ödeme kaynağıyla siparişi alırız. Bir ödeme kaynağı ayarlanmışsa, kimliğini alırız ve ilgili Paypal ödemesini PaypalPaymentService . Paypal ödemesi, onay URL'sini içerecektir. Herhangi bir ödeme kaynağı belirlenmemişse, siparişi tercih edilen ödeme yöntemi olarak Paypal ile güncelleriz. Ardından, PaypalPaymentService kullanarak sipariş için yeni bir Paypal ödemesi oluşturmaya devam ederiz. Buradan yeni oluşturulan siparişin onay url'sini alabiliriz.

Son olarak, kullanıcı butona tıkladığında satın alma işlemini onaylayabilecekleri Paypal'a yönlendirilir.

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

İşte şablonu.

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

Ödemeler sayfasının nasıl görüneceği aşağıda açıklanmıştır.

Ödeme sayfasının ekran görüntüsü
Ödeme sayfasının ekran görüntüsü. (Büyük önizleme)

Ödeme Bileşenini İptal Et

Paypal, bir ödeme iptal sayfası gerektirir. Bu bileşen bu amaca hizmet eder. Bu onun şablonu.

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

İşte sayfanın bir ekran görüntüsü.

Ödeme iptal sayfasının ekran görüntüsü
Ödeme iptal sayfasının ekran görüntüsü. (Büyük önizleme)

Sipariş Bileşeni

Bu, ödeme sürecindeki son adımdır. Burada kullanıcı gerçekten siparişi vermek ve işleme başlamak istediğini onaylar. Kullanıcı Paypal ödemesini onayladığında yönlendirildikleri sayfa budur. Paypal, url'ye bir ödeyen kimliği sorgu parametresi ekler. Bu, kullanıcının Paypal Kimliğidir.

Bileşen başlatıldığında, payerId sorgu parametresini alırız. Ardından sipariş, dahil edilen ödeme kaynağı ile OrderService kullanılarak alınır. Dahil edilen ödeme kaynağının kimliği, PaypalPayment hizmetini kullanarak Paypal ödemesini ödeyen kimliğiyle güncellemek için kullanılır. Bunlardan herhangi biri başarısız olursa, kullanıcı hata sayfasına yönlendirilir. Kullanıcının ödeme yapan kimliği ayarlanana kadar sipariş vermesini önlemek için disableButton özelliğini kullanıyoruz.

Sipariş ver düğmesine tıkladıklarında sipariş, placed durumuyla güncellenir. Sepet temizlendikten sonra başarılı bir snack bar görüntülenir ve kullanıcı ana sayfaya yönlendirilir.

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

İşte şablon ve ilgili stili.

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

İşte sayfanın bir ekran görüntüsü.

Sipariş yerleştirme sayfasının ekran görüntüsü
Sipariş yerleştirme sayfasının ekran görüntüsü. (Büyük önizleme)

Uygulama Modülü

Kimlik doğrulama dışında Ticaret Katmanına yapılan tüm isteklerin bir belirteç içermesi gerekir. Bu nedenle, uygulama başlatıldığı anda, sunucudaki /oauth/token yolundan bir jeton alınır ve bir oturum başlatılır. Belirtecin alındığı bir başlatma işlevi sağlamak için APP_INITIALIZER belirtecini kullanacağız. Ek olarak, daha önce oluşturduğumuz OptionsInterceptor sağlamak için HTTP_INTERCEPTORS belirtecini kullanacağız. Tüm modüller eklendikten sonra, uygulama modülü dosyası şöyle görünmelidir.

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

Uygulama Bileşeni

Burada bulabileceğiniz uygulama bileşeni şablonunu ve stilini değiştireceğiz.

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

Çözüm

Bu yazıda, Commerce Layer ve Paypal ile nasıl bir e-ticaret Angular 11 uygulaması oluşturabileceğinizi ele aldık. Ayrıca uygulamanın nasıl yapılandırılacağına ve bir e-ticaret API'si ile nasıl arayüz oluşturabileceğinize değindik.

Bu uygulama, bir müşterinin eksiksiz bir sipariş vermesine izin verse de, hiçbir şekilde bitmiş değildir. Geliştirmek için ekleyebileceğiniz çok şey var. Birincisi, alışveriş sepetindeki öğe miktarı değişikliklerini etkinleştirmeyi, alışveriş sepeti öğelerini ürün sayfalarına bağlamayı, adres bileşenlerini optimize etmeyi, sipariş verme sayfası gibi ödeme sayfaları için ek korumalar eklemeyi vb. seçebilirsiniz. Bu sadece başlangıç ​​noktası.

Baştan sona sipariş verme süreci hakkında daha fazla bilgi edinmek isterseniz, Ticaret Katmanı kılavuzlarına ve API'ye göz atabilirsiniz. Bu projenin kodunu bu depoda görüntüleyebilirsiniz.