Angular 11, Commerce Layer ve Paypal ile E-Ticaret Sitesi Nasıl Kurulur
Yayınlanan: 2022-03-10Gü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.
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.
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 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.
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.
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:
- çekirdek modül,
- veri modülü,
- paylaşılan modül,
- özellik modülleri.
Özellik modülleri, ilgili sayfaları ve bileşenleri birlikte gruplayacaktır. Dört özellik modülü olacaktır:
- yetkilendirme modülü,
- ürün modülü,
- sepet modülü,
- ö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.
Not Found Component
This is a 404 page that the user gets redirected to when they request a route not available on the router. Only its template is modified.
<app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>
Header Component
The HeaderComponent is basically the header displayed at the top of a page. It will contain the app title, the cart, login, and logout buttons.
When this component is initialized, a request is made to check whether the user has a current session. This happens when subscribing to this.session.isCustomerLoggedIn()
. We subscribe to this.session.loggedInStatus
to check if the user logs out throughout the life of the app. The this.header.showHeaderButtons
subscription decides whether to show all the buttons on the header or hide them. this.cart.cartValue$
gets the count of items in the cart.
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 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.
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.
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ü.
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ü.
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ü.
Ö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ü.
Ö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ü.
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.
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.
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.
Ö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 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ü.
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ü.
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.