Cara Membangun Situs E-Commerce Dengan Angular 11, Commerce Layer Dan Paypal

Diterbitkan: 2022-03-10
Ringkasan cepat Memiliki toko e-niaga sangat penting bagi setiap pemilik toko karena semakin banyak pelanggan yang beralih ke belanja online. Dalam tutorial ini, kita akan membahas cara membuat situs e-niaga dengan Angular 11. Situs tersebut akan menggunakan Commerce Layer sebagai API e-niaga tanpa kepala dan menggunakan Paypal untuk memproses pembayaran.

Saat ini sangat penting untuk memiliki kehadiran online ketika menjalankan bisnis. Belanja lebih banyak dilakukan secara online dibandingkan tahun-tahun sebelumnya. Memiliki toko e-niaga memungkinkan pemilik toko untuk membuka aliran pendapatan lain yang tidak dapat mereka manfaatkan hanya dengan toko batu bata dan mortir. Namun, pemilik toko lain menjalankan bisnis mereka secara online sepenuhnya tanpa kehadiran fisik. Hal ini membuat memiliki sebuah toko online penting.

Situs seperti Etsy, Shopify, dan Amazon memudahkan untuk menyiapkan toko dengan cukup cepat tanpa harus khawatir mengembangkan situs. Namun, mungkin ada kasus di mana pemilik toko mungkin menginginkan pengalaman yang dipersonalisasi atau mungkin menghemat biaya memiliki toko di beberapa platform ini.

Platform API e-niaga tanpa kepala menyediakan backend yang dapat digunakan situs toko untuk berinteraksi. Mereka mengelola semua proses dan data yang terkait dengan toko seperti pelanggan, pesanan, pengiriman, pembayaran, dan sebagainya. Yang diperlukan hanyalah antarmuka untuk berinteraksi dengan informasi ini. Ini memberi pemilik banyak fleksibilitas dalam memutuskan bagaimana pelanggan mereka akan mengalami toko online mereka dan bagaimana mereka memilih untuk menjalankannya.

Pada artikel ini, kita akan membahas bagaimana membangun toko e-commerce menggunakan Angular 11. Kita akan menggunakan Commerce Layer sebagai API e-commerce tanpa kepala kita. Meskipun mungkin ada banyak cara untuk memproses pembayaran, kami akan menunjukkan cara menggunakan hanya satu, Paypal.

  • Lihat kode sumber di GitHub →

Prasyarat

Sebelum membangun aplikasi, Anda harus menginstal Angular CLI. Kami akan menggunakannya untuk menginisialisasi dan membuat perancah aplikasi. Jika Anda belum menginstalnya, Anda bisa mendapatkannya melalui npm.

 npm install -g @angular/cli

Anda juga memerlukan akun pengembang Commerce Layer. Dengan menggunakan akun pengembang, Anda perlu membuat organisasi pengujian dan menyemainya dengan data pengujian. Penyemaian memudahkan pengembangan aplikasi terlebih dahulu tanpa mengkhawatirkan data apa yang harus Anda gunakan. Anda dapat membuat akun di tautan ini dan organisasi di sini.

Dasbor organisasi akun pengembang Commerce Layer
Dasbor organisasi akun pengembang Commerce Layer tempat Anda menambahkan organisasi. (Pratinjau besar)
Formulir pembuatan organisasi Lapisan Perdagangan
Centang kotak Benih dengan data uji saat membuat organisasi baru. (Pratinjau besar)

Terakhir, Anda memerlukan akun Sandbox Paypal. Memiliki jenis akun ini akan memungkinkan kami untuk menguji transaksi antara bisnis dan pengguna tanpa mempertaruhkan uang yang sebenarnya. Anda dapat membuatnya di sini. Akun kotak pasir memiliki bisnis uji coba dan akun pribadi uji coba yang sudah dibuat untuknya.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Lapisan Perdagangan Dan Konfigurasi Paypal

Untuk memungkinkan pembayaran Paypal Sandbox di Commerce Layer, Anda harus menyiapkan kunci API. Buka ikhtisar akun akun pengembang Paypal Anda. Pilih akun bisnis dan di bawah tab kredensial API pada detail akun, Anda akan menemukan Aplikasi Default di bawah Aplikasi REST .

Tab Kredensial API pada pop-up detail akun bisnis Paypal Sandbox
Di mana menemukan aplikasi REST default pada pop-up detail akun bisnis Paypal. (Pratinjau besar)
Ikhtisar Aplikasi Default pada pengaturan akun bisnis Paypal Sandbox
Ikhtisar Aplikasi Default pada pengaturan akun bisnis Paypal Sandbox di mana Anda bisa mendapatkan ID klien dan rahasia REST API. (Pratinjau besar)

Untuk mengaitkan akun bisnis Paypal Anda dengan organisasi Lapisan Komersial Anda, buka dasbor organisasi Anda. Di sini Anda akan menambahkan gateway pembayaran Paypal dan metode pembayaran Paypal untuk berbagai pasar Anda. Di bawah Pengaturan> Pembayaran , pilih Gateway Pembayaran> Paypal dan tambahkan ID dan rahasia klien Paypal Anda.

Dasbor Gateway Pembayaran baru di Commerce Layer
Dimana pada dashboard Commerce Layer untuk membuat gateway pembayaran Paypal. (Pratinjau besar)

Setelah membuat gateway, Anda perlu membuat metode pembayaran Paypal untuk setiap pasar yang Anda targetkan agar Paypal tersedia sebagai opsi. Anda akan melakukannya di Setelan > Pembayaran > Metode Pembayaran > Metode Pembayaran Baru .

Dasbor Metode Pembayaran di Commerce Layer
Dimana pada dashboard Commerce Layer untuk membuat metode pembayaran Paypal. (Pratinjau besar)

Catatan Tentang Rute yang Digunakan

Commerce Layer menyediakan rute untuk autentikasi dan kumpulan rute lain yang berbeda untuk API mereka. Rute otentikasi /oauth/token mereka menukar kredensial dengan token. Token ini diperlukan untuk mengakses API mereka. Rute API lainnya mengambil pola /api/:resource .

Cakupan artikel ini hanya mencakup bagian frontend dari aplikasi ini. Saya memilih untuk menyimpan sisi server token, menggunakan sesi untuk melacak kepemilikan, dan menyediakan cookie http-saja dengan id sesi kepada klien. Ini tidak akan dibahas di sini karena berada di luar cakupan artikel ini. Namun, rute tetap sama dan persis sesuai dengan Commerce Layer API. Meskipun, ada beberapa rute khusus yang tidak tersedia dari Commerce Layer API yang akan kami gunakan. Ini terutama berhubungan dengan manajemen sesi. Saya akan menunjukkan ini saat kita membahasnya dan menjelaskan bagaimana Anda dapat mencapai hasil yang serupa.

Inkonsistensi lain yang mungkin Anda perhatikan adalah bahwa badan permintaan berbeda dari yang dibutuhkan Commerce Layer API. Karena permintaan diteruskan ke server lain untuk diisi dengan token, saya menyusun badan secara berbeda. Ini untuk memudahkan pengiriman permintaan. Setiap kali ada inkonsistensi dalam badan permintaan, ini akan ditunjukkan dalam layanan.

Karena ini di luar cakupan, Anda harus memutuskan cara menyimpan token dengan aman. Anda juga perlu sedikit memodifikasi badan permintaan agar sesuai dengan apa yang dibutuhkan Commerce Layer API. Ketika ada inkonsistensi, saya akan menautkan ke referensi API dan panduan yang merinci cara menyusun tubuh dengan benar.

Struktur Aplikasi

Untuk mengatur aplikasi, kami akan memecahnya menjadi empat bagian utama. Deskripsi yang lebih baik tentang apa yang dilakukan masing-masing modul diberikan di bawah bagian yang sesuai:

  1. modul inti,
  2. modul datanya,
  3. modul bersama,
  4. modul fitur.

Modul fitur akan mengelompokkan halaman dan komponen terkait bersama-sama. Akan ada empat modul fitur:

  1. modul otentikasi,
  2. modul produk,
  3. modul gerobak,
  4. modul pembayaran.

Saat kita masuk ke setiap modul, saya akan menjelaskan apa tujuannya dan merinci isinya.

Di bawah ini adalah pohon folder src/app dan di mana setiap modul berada.

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

Menghasilkan Aplikasi Dan Menambahkan Ketergantungan

Kami akan mulai dengan membuat aplikasi. Organisasi kami akan disebut The LIme Brand dan akan memiliki data uji yang sudah diunggulkan oleh Commerce Layer.

 ng new lime-app

Kita akan membutuhkan beberapa dependensi. Terutama Bahan Sudut dan Sampai Hancur. Bahan Sudut akan menyediakan komponen dan gaya. Hingga Destroy secara otomatis berhenti berlangganan dari yang dapat diamati ketika komponen dihancurkan. Untuk menginstalnya jalankan:

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

Aktiva

Saat menambahkan alamat ke Commerce Layer, kode negara alfa-2 harus digunakan. Kami akan menambahkan file json yang berisi kode-kode ini ke folder assets di assets/json/country-codes.json . Anda dapat menemukan file ini ditautkan di sini.

Gaya

Komponen yang akan kita buat berbagi beberapa gaya global. Kami akan menempatkannya di styles.css yang dapat ditemukan di tautan ini.

Lingkungan

Konfigurasi kami akan terdiri dari dua bidang. apiUrl yang seharusnya mengarah ke Commerce Layer API. apiUrl digunakan oleh layanan yang akan kita buat untuk mengambil data. clientUrl harus menjadi domain tempat aplikasi berjalan. Kami menggunakan ini saat mengatur URL pengalihan untuk Paypal. Anda dapat menemukan file ini di tautan ini.

Modul Bersama

Modul bersama akan berisi layanan, pipa, dan komponen yang dibagikan di seluruh modul lainnya.

 ng gm shared

Ini terdiri dari tiga komponen, satu pipa, dan dua layanan. Inilah yang akan terlihat seperti.

 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

Kami juga akan menggunakan modul bersama untuk mengekspor beberapa komponen Bahan Sudut yang umum digunakan. Ini membuatnya lebih mudah untuk menggunakannya di luar kotak daripada mengimpor setiap komponen di berbagai modul. Inilah yang akan dikandung oleh shared.module.ts .

 @NgModule({ declarations: [SimplePageComponent, TitleComponent, WordWrapPipe, ItemQuantityComponent], imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, RouterModule], exports: [ CommonModule, ItemQuantityComponent, MatButtonModule, MatIconModule, MatSnackBarModule, MatTooltipModule, SimplePageComponent, TitleComponent, WordWrapPipe ] }) export class SharedModule { }

Komponen

Komponen Kuantitas Barang

Komponen ini mengatur jumlah item saat menambahkannya ke keranjang. Ini akan digunakan dalam modul troli dan produk. Sebuah pemilih material akan menjadi pilihan yang mudah untuk tujuan ini. Namun, gaya pemilihan material tidak cocok dengan input material yang digunakan dalam semua bentuk lainnya. Menu material terlihat sangat mirip dengan input material yang digunakan. Jadi saya memutuskan untuk membuat komponen pilih dengannya.

 ng gc shared/components/item-quantity

Komponen akan memiliki tiga properti input dan satu properti output. quantity menetapkan jumlah awal item, maxValue menunjukkan jumlah maksimum item yang dapat dipilih sekaligus, dan disabled menunjukkan apakah komponen harus dinonaktifkan atau tidak. setQuantityEvent dipicu ketika kuantitas dipilih.

Saat komponen diinisialisasi, kami akan mengatur nilai yang muncul di menu material. Ada juga metode yang disebut setQuantity yang akan memancarkan peristiwa setQuantityEvent .

Ini adalah file komponen.

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

Ini dia templatenya.

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

Berikut styling-nya.

 button { margin: 3px; }

Komponen Judul

Komponen ini berfungsi ganda sebagai judul stepper serta judul biasa pada beberapa halaman yang lebih sederhana. Meskipun Angular Material menyediakan komponen stepper, itu bukan yang paling cocok untuk proses checkout yang agak lama, tidak responsif pada tampilan yang lebih kecil, dan membutuhkan lebih banyak waktu untuk mengimplementasikannya. Namun, judul yang lebih sederhana dapat digunakan kembali sebagai indikator langkah dan berguna di banyak halaman.

 ng gc shared/components/title

Komponen memiliki empat properti input: title , subtitle , angka ( no ), dan centerText , untuk menunjukkan apakah teks komponen akan di tengah atau tidak.

 @Component({ selector: 'app-title', templateUrl: './title.component.html', styleUrls: ['./title.component.css'] }) export class TitleComponent { @Input() title: string = ''; @Input() subtitle: string = ''; @Input() no?: string; @Input() centerText?: boolean = false; }

Di bawah ini adalah template-nya. Anda dapat menemukan gayanya ditautkan di sini.

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

Komponen Halaman Sederhana

Ada beberapa contoh di mana hanya judul, ikon, dan tombol yang diperlukan untuk sebuah halaman. Ini termasuk halaman 404, halaman keranjang kosong, halaman kesalahan, halaman pembayaran, dan halaman penempatan pesanan. Itulah tujuan yang akan dilayani oleh komponen halaman sederhana. Saat tombol pada halaman diklik, tombol tersebut akan mengarahkan ulang ke rute atau melakukan beberapa tindakan sebagai respons terhadap buttonEvent .

Untuk membuatnya:

 ng gc shared/components/simple-page

Ini adalah file komponennya.

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

Dan templatenya berisi:

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

Gayanya dapat ditemukan di sini.

pipa

Pipa Bungkus Kata

Beberapa nama produk dan jenis informasi lain yang ditampilkan di situs sangat panjang. Dalam beberapa kasus, membuat kalimat-kalimat yang panjang ini menjadi komponen-komponen material itu sulit. Jadi kita akan menggunakan pipa ini untuk memotong kalimat menjadi panjang tertentu dan menambahkan elips ke akhir hasil.

Untuk membuatnya jalankan:

 ng g pipe shared/pipes/word-wrap

Ini akan berisi:

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

Jasa

Layanan Penangan Kesalahan HTTP

Ada cukup banyak layanan http dalam proyek ini. Membuat penangan kesalahan untuk setiap metode berulang. Jadi membuat satu penangan tunggal yang dapat digunakan oleh semua metode masuk akal. Penangan kesalahan dapat digunakan untuk memformat kesalahan dan juga meneruskan kesalahan ke platform logging eksternal lainnya.

Hasilkan dengan menjalankan:

 ng gs shared/services/http-error-handler

Layanan ini hanya akan berisi satu metode. Metode ini akan memformat pesan kesalahan yang akan ditampilkan tergantung pada apakah itu kesalahan klien atau server. Namun, ada ruang untuk meningkatkannya lebih lanjut.

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

Layanan Penyimpanan Lokal

Kami akan menggunakan penyimpanan lokal untuk melacak jumlah item dalam keranjang. Ini juga berguna untuk menyimpan ID pesanan di sini. Pesanan sesuai dengan keranjang di Commerce Layer.

Untuk menghasilkan layanan penyimpanan lokal, jalankan:

 ng gs shared/services/local-storage

Layanan ini akan berisi empat metode untuk menambah, menghapus, dan mendapatkan item dari penyimpanan lokal dan satu lagi untuk menghapusnya.

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

Modul Data

Modul ini bertanggung jawab untuk pengambilan dan pengelolaan data. Itu yang akan kami gunakan untuk mendapatkan data yang digunakan aplikasi kami. Di bawah ini adalah strukturnya:

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

Untuk menghasilkan modul, jalankan:

 ng gm data

model

Model menentukan bagaimana data yang kita konsumsi dari API terstruktur. Kami akan memiliki 16 deklarasi antarmuka. Untuk membuatnya jalankan:

 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

Tabel berikut menautkan ke setiap file dan memberikan deskripsi tentang masing-masing antarmuka.

Antarmuka Keterangan
Alamat Mewakili alamat umum.
Keranjang Versi sisi klien dari pesanan yang melacak jumlah produk yang ingin dibeli pelanggan.
Negara Kode negara alfa-2.
Alamat pelanggan Alamat yang terkait dengan pelanggan.
Pelanggan Seorang pengguna terdaftar.
Waktu Pengiriman: Merupakan jumlah waktu yang diperlukan untuk pengiriman kiriman.
Item baris Produk yang diperinci ditambahkan ke troli.
Memesan Keranjang belanja atau kumpulan item baris.
Cara Pembayaran Jenis pembayaran yang tersedia untuk pesanan.
Sumber Pembayaran Pembayaran yang terkait dengan pesanan.
Pembayaran Paypal Pembayaran dilakukan melalui Paypal
Harga Harga terkait dengan SKU.
Pengiriman Koleksi item dikirim bersama-sama.
metode pengiriman Metode pengiriman paket.
SKU Unit penyimpanan stok yang unik.
Lokasi Stok Lokasi yang berisi inventaris SKU.

Jasa

Folder ini berisi layanan yang membuat, mengambil, dan memanipulasi data aplikasi. Kami akan membuat 11 layanan di sini.

 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

Layanan Alamat

Layanan ini membuat dan mengambil alamat. Ini penting saat membuat dan menetapkan alamat pengiriman dan penagihan ke pesanan. Ini memiliki dua metode. Satu untuk membuat alamat dan satu lagi untuk mengambilnya.

Rute yang digunakan di sini adalah /api/addresses . Jika Anda akan menggunakan Commerce Layer API secara langsung, pastikan untuk menyusun data seperti yang ditunjukkan dalam contoh ini.

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

Layanan Kereta

Troli bertanggung jawab untuk menjaga jumlah item yang ditambahkan dan ID pesanan. Melakukan panggilan API untuk mendapatkan jumlah item dalam pesanan setiap kali item baris baru dibuat bisa mahal. Sebagai gantinya, kami hanya dapat menggunakan penyimpanan lokal untuk mempertahankan jumlah klien. Ini menghilangkan kebutuhan untuk melakukan pengambilan pesanan yang tidak perlu setiap kali item ditambahkan ke troli.

Kami juga menggunakan layanan ini untuk menyimpan ID pesanan. Sebuah keranjang sesuai dengan pesanan di Commerce Layer. Setelah item pertama ditambahkan ke troli, pesanan dibuat. Kami perlu mempertahankan ID pesanan ini sehingga kami dapat mengambilnya selama proses checkout.

Selain itu, kita membutuhkan cara untuk berkomunikasi dengan header bahwa item telah ditambahkan ke keranjang. Header berisi tombol cart dan menampilkan jumlah item di dalamnya. Kami akan menggunakan Observable dari BehaviorSubject dengan nilai cart saat ini. Header dapat berlangganan ini dan melacak perubahan nilai keranjang.

Terakhir, setelah pesanan selesai, nilai keranjang harus dibersihkan. Ini memastikan bahwa tidak ada kebingungan saat membuat pesanan baru berikutnya. Nilai yang disimpan dihapus setelah pesanan saat ini ditandai sebagai ditempatkan.

Kami akan menyelesaikan semua ini menggunakan layanan penyimpanan lokal yang dibuat sebelumnya.

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

Layanan Negara

Saat menambahkan alamat di Commerce Layer, kode negara harus berupa kode alfa 2. Layanan ini membaca file json yang berisi kode-kode ini untuk setiap negara dan mengembalikannya dalam metode getCountries .

 @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } } @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } } @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } }

Layanan Alamat Pelanggan

Layanan ini digunakan untuk menghubungkan alamat dengan pelanggan. Itu juga mengambil alamat tertentu atau semua yang terkait dengan pelanggan. Ini digunakan ketika pelanggan menambahkan alamat pengiriman dan penagihan ke pesanan mereka. Metode createCustomer membuat pelanggan, getCustomerAddresses mendapatkan semua alamat pelanggan, dan getCustomerAddress mendapatkan yang spesifik.

Saat membuat alamat pelanggan, pastikan untuk menyusun badan pos sesuai dengan contoh ini.

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

Pelayanan pelanggan

Pelanggan dibuat dan informasi mereka diambil menggunakan layanan ini. Saat pengguna mendaftar, mereka menjadi pelanggan dan dibuat menggunakan createCustomerMethod . getCustomer mengembalikan pelanggan yang terkait dengan ID tertentu. getCurrentCustomer mengembalikan pelanggan yang saat ini masuk.

Saat membuat pelanggan, buat struktur data seperti ini. Anda dapat menambahkan nama depan dan belakang mereka ke metadata, seperti yang ditunjukkan pada atributnya.

Rute /api/customers/current tidak tersedia di Commerce Layer. Jadi, Anda harus mencari cara untuk mendapatkan pelanggan yang saat ini masuk.

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

Layanan Lead Time Pengiriman

Layanan ini mengembalikan informasi tentang jadwal pengiriman dari berbagai lokasi stok.

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

Layanan Item Baris

Item yang ditambahkan ke keranjang dikelola oleh layanan ini. Dengan itu, Anda dapat membuat item saat ditambahkan ke troli. Informasi item juga dapat diambil. Item juga dapat diperbarui ketika jumlahnya berubah atau dihapus saat dikeluarkan dari keranjang.

Saat membuat item atau memperbaruinya, susun badan permintaan seperti yang ditunjukkan dalam contoh ini.

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

Layanan Pemesanan

Mirip dengan layanan item baris, layanan pesanan memungkinkan Anda membuat, memperbarui, menghapus, atau mendapatkan pesanan. Selain itu, Anda dapat memilih untuk mendapatkan pengiriman yang terkait dengan pesanan secara terpisah menggunakan metode getOrderShipments . Layanan ini banyak digunakan selama proses checkout.

Ada berbagai jenis informasi tentang pesanan yang diperlukan selama checkout. Karena mungkin mahal untuk mengambil seluruh pesanan dan hubungannya, kami menentukan apa yang ingin kami dapatkan dari pesanan menggunakan GetOrderParams . Setara dengan ini di CL API adalah parameter kueri sertakan tempat Anda mencantumkan hubungan urutan yang akan disertakan. Anda dapat memeriksa bidang apa yang perlu disertakan untuk ringkasan keranjang di sini dan untuk berbagai tahap pembayaran di sini.

Dengan cara yang sama, saat memperbarui pesanan, kami menggunakan UpdateOrderParams untuk menentukan bidang pembaruan. Ini karena di server yang mengisi token, beberapa operasi tambahan dilakukan tergantung pada bidang apa yang sedang diperbarui. Namun, jika Anda membuat permintaan langsung ke CL API, Anda tidak perlu menentukan ini. Anda dapat menghapusnya karena CL API tidak mengharuskan Anda untuk menentukannya. Meskipun, badan permintaan harus menyerupai contoh ini.

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

Layanan Pembayaran Paypal

Layanan ini bertanggung jawab untuk membuat dan memperbarui pembayaran Paypal untuk pesanan. Selain itu, kita bisa mendapatkan pembayaran Paypal yang diberikan id-nya. Badan pos harus memiliki struktur yang mirip dengan contoh ini saat membuat pembayaran Paypal.

 @Injectable({ providedIn: 'root' }) export class PaypalPaymentService { private url: string = `${environment.apiUrl}/api/paypal_payments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createPaypalPayment(payment: PaypalPayment): Observable<PaypalPayment> { return this.http.post<PaypalPayment>(this.url, payment) .pipe(catchError(this.eh.handleError)); } getPaypalPayment(id: string): Observable<PaypalPayment> { return this.http.get<PaypalPayment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updatePaypalPayment(id: string, paypalPayerId: string): Observable<PaypalPayment> { return this.http.patch<PaypalPayment>( `${this.url}/${id}`, { paypalPayerId: paypalPayerId } ) .pipe(catchError(this.eh.handleError)); } }

Layanan pengiriman

Layanan ini mendapat kiriman atau memperbaruinya dengan id-nya. Badan permintaan pembaruan pengiriman akan terlihat mirip dengan contoh ini.

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

Layanan SKU

Layanan SKU mendapatkan produk dari toko. Jika beberapa produk sedang diambil, mereka dapat diberi halaman dan memiliki ukuran halaman yang ditetapkan. Ukuran halaman dan nomor halaman harus disetel sebagai parameter kueri seperti dalam contoh ini jika Anda membuat permintaan langsung ke API. Satu produk juga dapat diambil dengan id-nya.

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

Modul Inti

Modul inti berisi segala sesuatu yang penting dan umum di seluruh aplikasi. Ini termasuk komponen seperti header dan halaman seperti halaman 404. Layanan yang bertanggung jawab untuk otentikasi dan manajemen sesi juga termasuk di sini, serta pencegat dan penjaga di seluruh aplikasi.

Pohon modul inti akan terlihat seperti ini.

 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

Untuk menghasilkan modul dan isinya, jalankan:

 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

File modul inti harus seperti ini. Perhatikan bahwa rute telah didaftarkan untuk NotFoundComponent dan ErrorComponent .

 @NgModule({ declarations: [HeaderComponent, NotFoundComponent, ErrorComponent], imports: [ RouterModule.forChild([ { path: '404', component: NotFoundComponent }, { path: 'error', component: ErrorComponent }, { path: '**', redirectTo: '/404' } ]), MatBadgeModule, SharedModule ], exports: [HeaderComponent] }) export class CoreModule { }

Jasa

Folder layanan menyimpan layanan otentikasi, sesi, dan header.

Layanan Otentikasi

AuthenticationService memungkinkan Anda memperoleh token klien dan pelanggan. Token ini digunakan untuk mengakses sisa rute API. Token pelanggan dikembalikan ketika pengguna menukar email dan kata sandi untuk itu dan memiliki jangkauan izin yang lebih luas. Token klien dikeluarkan tanpa memerlukan kredensial dan memiliki izin yang lebih sempit.

getClientSession mendapatkan token klien. login mendapat token pelanggan. Kedua metode juga membuat sesi. Tubuh permintaan token klien akan terlihat seperti ini dan token pelanggan seperti ini.

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

Komponen

Error Component

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

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

This is what the component will look like.

Screenshot of error page
Screenshot of error page. (Pratinjau besar)

Not Found Component

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

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

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.

Ada metode logout yang menghancurkan sesi pengguna dan memberi mereka token klien. Token klien ditetapkan karena sesi yang mempertahankan token pelanggan mereka dihancurkan dan token masih diperlukan untuk setiap permintaan API. Snackbar materi berkomunikasi kepada pengguna apakah sesi mereka berhasil dihancurkan atau tidak.

Kami menggunakan @UntilDestroy({ checkProperties: true }) untuk menunjukkan bahwa semua langganan harus secara otomatis berhenti berlangganan sejak komponen dihancurkan.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'] }) export class HeaderComponent implements OnInit { cartAmount: number = 0; isLoggedIn: boolean = false; showButtons: boolean = true; constructor( private session: SessionService, private snackBar: MatSnackBar, private cart: CartService, private header: HeaderService, private auth: AuthenticationService ) { } ngOnInit() { this.session.isCustomerLoggedIn() .subscribe( () => { this.isLoggedIn = true; this.session.setLoggedInStatus(true); } ); this.session.loggedInStatus.subscribe(status => this.isLoggedIn = status); this.header.showHeaderButtons.subscribe(visible => this.showButtons = visible); this.cart.cartValue$.subscribe(cart => this.cartAmount = cart.itemCount); } logout() { concat( this.session.logout(), this.auth.getClientSession() ).subscribe( () => { this.snackBar.open('You have been logged out.', 'Close', { duration: 4000 }); this.session.setLoggedInStatus(false); }, err => this.snackBar.open('There was a problem logging you out.', 'Close', { duration: 4000 }) ); } }

Di bawah ini adalah templat tajuk dan yang ditautkan di sini adalah gayanya.

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

Penjaga

Penjaga Kereta Kosong

Penjaga ini mencegah pengguna mengakses rute yang berkaitan dengan checkout dan penagihan jika keranjang mereka kosong. Ini karena untuk melanjutkan checkout, perlu ada pesanan yang valid. Pesanan sesuai dengan keranjang dengan item di dalamnya. Jika ada item di keranjang, pengguna dapat melanjutkan ke halaman yang dijaga. Namun, jika keranjang kosong, pengguna akan diarahkan ke halaman keranjang kosong.

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

pencegat

Opsi pencegat

Pencegat ini mencegat semua permintaan HTTP keluar dan menambahkan dua opsi ke permintaan. Ini adalah header Content-Type dan properti withCredentials . withCredentials menentukan apakah permintaan harus dikirim dengan kredensial keluar seperti cookie http-saja yang kami gunakan. Kami menggunakan Content-Type untuk menunjukkan bahwa kami mengirim sumber daya json ke server.

 @Injectable() export class OptionsInterceptor implements HttpInterceptor { constructor() { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json'), withCredentials: true }); return next.handle(request); } }

Modul Fitur

Bagian ini berisi fitur utama aplikasi. Seperti disebutkan sebelumnya, fitur dikelompokkan dalam empat modul: modul auth, product, cart, dan checkout.

Modul Produk

Modul produk berisi halaman yang menampilkan produk yang dijual. Ini termasuk halaman produk dan halaman daftar produk. Ini terstruktur seperti yang ditunjukkan di bawah ini.

 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

Untuk menghasilkannya dan komponennya:

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

Ini adalah file modul:

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

Komponen Daftar Produk

Komponen ini menampilkan daftar paginasi produk yang tersedia untuk dijual. Ini adalah halaman pertama yang dimuat saat aplikasi dimulai.

Produk ditampilkan dalam kotak. Daftar kisi bahan adalah komponen terbaik untuk ini. Untuk membuat grid responsif, jumlah kolom grid akan berubah tergantung pada ukuran layar. Layanan BreakpointObserver memungkinkan kita untuk menentukan ukuran layar dan menetapkan kolom selama inisialisasi.

Untuk mendapatkan produk, kami memanggil metode getProducts dari SkuService . Ini mengembalikan produk jika berhasil dan menetapkannya ke grid. Jika tidak, kami mengarahkan pengguna ke halaman kesalahan.

Karena produk yang ditampilkan diberi halaman, kami akan memiliki metode getNextPage untuk mendapatkan produk tambahan.

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

Template ditunjukkan di bawah ini dan gayanya dapat ditemukan di sini.

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

Halamannya akan terlihat seperti ini.

Tangkapan layar halaman daftar produk
Tangkapan layar halaman daftar produk. (Pratinjau besar)

Komponen Produk

Setelah produk dipilih dari halaman daftar produk, komponen ini menampilkan detailnya. Ini termasuk nama lengkap produk, harga, dan deskripsi. Ada juga tombol untuk menambahkan item ke keranjang produk.

Pada inisialisasi, kami mendapatkan id produk dari parameter rute. Menggunakan id, kami mengambil produk dari SkuService .

Saat pengguna menambahkan item ke keranjang, metode addItemToCart dipanggil. Di dalamnya, kami memeriksa apakah pesanan telah dibuat untuk keranjang. Jika tidak, yang baru dibuat menggunakan OrderService . Setelah itu, item baris dibuat dalam urutan yang sesuai dengan produk. Jika pesanan sudah ada untuk keranjang, hanya item baris yang dibuat. Tergantung pada status permintaan, pesan snackbar ditampilkan kepada pengguna.

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

Template ProductComponent adalah sebagai berikut dan gayanya ditautkan di sini.

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

Halamannya akan terlihat seperti ini.

Tangkapan layar halaman produk
Tangkapan layar halaman produk. (Pratinjau besar)

Modul Otentikasi

Modul Auth berisi halaman yang bertanggung jawab untuk otentikasi. Ini termasuk halaman login dan pendaftaran. Ini terstruktur sebagai berikut.

 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

Untuk menghasilkannya dan komponennya:

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

Ini adalah file modulnya.

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

Komponen Pendaftaran

Seorang pengguna mendaftar untuk sebuah akun menggunakan komponen ini. Nama depan, nama belakang, email, dan kata sandi diperlukan untuk proses ini. Pengguna juga perlu mengkonfirmasi kata sandi mereka. Bidang input akan dibuat dengan layanan FormBuilder . Validasi ditambahkan untuk mengharuskan semua input memiliki nilai. Validasi tambahan ditambahkan ke bidang kata sandi untuk memastikan panjang minimum delapan karakter. Validator matchPasswords khusus memastikan bahwa kata sandi yang dikonfirmasi cocok dengan kata sandi awal.

Saat komponen diinisialisasi, tombol cart, login, dan logout di header disembunyikan. Ini dikomunikasikan ke header menggunakan HeaderService .

Setelah semua bidang ditandai sebagai valid, pengguna kemudian dapat mendaftar. Dalam metode signup , metode createCustomer dari CustomerService menerima input ini. Jika pendaftaran berhasil, pengguna diberitahu bahwa akun mereka berhasil dibuat menggunakan snackbar. Mereka kemudian dialihkan ke halaman rumah.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-signup', templateUrl: './signup.component.html', styleUrls: ['./signup.component.css'] }) export class SignupComponent implements OnInit { signupForm = this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], confirmedPassword: ['', [Validators.required]] }, { validators: this.matchPasswords }); @ViewChild(FormGroupDirective) sufDirective: FormGroupDirective | undefined; constructor( private customer: CustomerService, private fb: FormBuilder, private snackBar: MatSnackBar, private router: Router, private header: HeaderService ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } matchPasswords(signupGroup: AbstractControl): ValidationErrors | null { const password = signupGroup.get('password')?.value; const confirmedPassword = signupGroup.get('confirmedPassword')?.value; return password == confirmedPassword ? null : { differentPasswords: true }; } get password() { return this.signupForm.get('password'); } get confirmedPassword() { return this.signupForm.get('confirmedPassword'); } signup() { const customer = this.signupForm.value; this.customer.createCustomer( customer.email, customer.password, customer.firstName, customer.lastName ).subscribe( () => { this.signupForm.reset(); this.sufDirective?.resetForm(); this.snackBar.open('Account successfully created. You will be redirected in 5 seconds.', 'Close', { duration: 5000 }); setTimeout(() => this.router.navigateByUrl('/'), 6000); }, err => this.snackBar.open('There was a problem creating your account.', 'Close', { duration: 5000 }) ); } }

Di bawah ini adalah template untuk SignupComponent .

 <form [formGroup]="signupForm" (ngSubmit)="signup()"> <h1 class="mat-display-3">Create Account</h1> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput formControlName="email" type="email"> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput formControlName="password" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Confirm Password</mat-label> <input matInput formControlName="confirmedPassword" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <div *ngIf="confirmedPassword?.invalid && (confirmedPassword?.dirty || confirmedPassword?.touched)"> <mat-error *ngIf="signupForm.hasError('differentPasswords')"> Your passwords do not match. </mat-error> </div> <div *ngIf="password?.invalid && (password?.dirty || password?.touched)"> <mat-error *ngIf="password?.hasError('minlength')"> Your password should be at least 8 characters. </mat-error> </div> <button mat-flat-button color="primary" [disabled]="!signupForm.valid">Sign Up</button> </form>

Komponen akan berubah sebagai berikut.

Tangkapan layar halaman pendaftaran
Tangkapan layar halaman pendaftaran. (Pratinjau besar)

Komponen Masuk

Pengguna terdaftar masuk ke akun mereka dengan komponen ini. Email dan kata sandi harus dimasukkan. Bidang input yang sesuai akan memiliki validasi yang membuatnya diperlukan.

Mirip dengan SignupComponent , tombol cart, login, dan logout di header disembunyikan. Visibilitasnya diatur menggunakan HeaderService selama inisialisasi komponen.

Untuk masuk, kredensial diteruskan ke AuthenticationService . Jika berhasil, status login pengguna diatur menggunakan SessionService . Pengguna kemudian diarahkan kembali ke halaman mereka berada. Jika tidak berhasil, snackbar ditampilkan dengan kesalahan dan bidang kata sandi diatur ulang.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { loginForm = this.fb.group({ email: ['', Validators.required], password: ['', Validators.required] }); constructor( private authService: AuthenticationService, private session: SessionService, private snackBar: MatSnackBar, private fb: FormBuilder, private header: HeaderService, private location: Location ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } login() { const credentials = this.loginForm.value; this.authService.login( credentials.email, credentials.password ).subscribe( () => { this.session.setLoggedInStatus(true); this.location.back(); }, err => { this.snackBar.open( 'Login failed. Check your login credentials.', 'Close', { duration: 6000 }); this.loginForm.patchValue({ password: '' }); } ); } }

Di bawah ini adalah template LoginComponent .

 <form [formGroup]="loginForm" (ngSubmit)="login()"> <h1 class="mat-display-3">Login</h1> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput type="email" formControlName="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput type="password" formControlName="password" required> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="!loginForm.valid">Login</button> <p class="mat-h3">Not registered yet? <a routerLink="/signup">Create an account.</a></p> </form>

Berikut adalah screenshot halaman tersebut.

Tangkapan layar halaman login
Tangkapan layar halaman login. (Pratinjau besar)

Modul Kereta

Modul cart berisi semua halaman yang berhubungan dengan cart. Ini termasuk halaman ringkasan pesanan, halaman kode kupon dan kartu hadiah, dan halaman keranjang kosong. Ini terstruktur sebagai berikut.

 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

Untuk menghasilkannya, jalankan:

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

Ini adalah file modul.

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

Komponen Kode

Seperti disebutkan sebelumnya, komponen ini digunakan untuk menambahkan kode kupon atau kartu hadiah ke pesanan. Ini memungkinkan pengguna untuk menerapkan diskon ke total pesanan mereka sebelum melanjutkan ke checkout.

Akan ada dua kolom input. Satu untuk kupon dan satu lagi untuk kode kartu hadiah.

Kode ditambahkan dengan memperbarui pesanan. Metode updateOrder dari OrderService memperbarui pesanan dengan kode. Setelah itu, kedua bidang diatur ulang dan pengguna diberitahu tentang keberhasilan operasi dengan snackbar. Snackbar juga ditampilkan saat terjadi kesalahan. Baik metode addCoupon maupun addGiftCard memanggil metode updateOrder .

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-codes', templateUrl: './codes.component.html', styleUrls: ['./codes.component.css'] }) export class CodesComponent { couponCode = new FormControl(''); giftCardCode = new FormControl(''); @ViewChild(FormControlDirective) codesDirective: FormControlDirective | undefined; constructor( private cart: CartService, private order: OrderService, private snackBar: MatSnackBar ) { } private updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) { this.order.updateOrder(order, params) .subscribe( () => { this.snackBar.open(`Successfully added ${codeType} code.`, 'Close', { duration: 8000 }); this.couponCode.reset(); this.giftCardCode.reset(); this.codesDirective?.reset(); }, err => this.snackBar.open(`There was a problem adding your ${codeType} code.`, 'Close', { duration: 8000 }) ); } addCoupon() { this.updateOrder({ id: this.cart.orderId, couponCode: this.couponCode.value }, [UpdateOrderParams.couponCode], 'coupon'); } addGiftCard() { this.updateOrder({ id: this.cart.orderId, giftCardCode: this.giftCardCode.value }, [UpdateOrderParams.giftCardCode], 'gift card'); } }

Template ditunjukkan di bawah ini dan gayanya dapat ditemukan di tautan ini.

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

Berikut adalah screenshot halaman tersebut.

Tangkapan layar halaman kode
Tangkapan layar halaman kode. (Pratinjau besar)

Komponen Kosong

Seharusnya tidak mungkin untuk check out dengan keranjang kosong. Perlu ada penjaga yang mencegah pengguna mengakses halaman modul checkout dengan gerobak kosong. Ini telah dibahas sebagai bagian dari CoreModule . Penjaga mengarahkan permintaan ke halaman checkout dengan keranjang kosong ke EmptyCartComponent .

Ini adalah komponen yang sangat sederhana yang memiliki beberapa teks yang menunjukkan kepada pengguna bahwa keranjang mereka kosong. Ini juga memiliki tombol yang dapat diklik pengguna untuk membuka beranda untuk menambahkan barang ke keranjang mereka. Jadi kita akan menggunakan SimplePageComponent untuk menampilkannya. Berikut adalah templatenya.

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

Berikut adalah screenshot halaman tersebut.

Tangkapan layar halaman keranjang kosong
Tangkapan layar halaman keranjang kosong. (Pratinjau besar)

Komponen Ringkasan

Komponen ini merangkum keranjang/pesanan. Ini daftar semua item di gerobak, nama mereka, jumlah, dan gambar. Ini juga merinci biaya pesanan termasuk pajak, pengiriman, dan diskon. Pengguna harus dapat melihat ini dan memutuskan apakah mereka puas dengan item dan biaya sebelum melanjutkan ke checkout.

Pada inisialisasi, pesanan dan item barisnya diambil menggunakan OrderService . Pengguna harus dapat mengubah item baris atau bahkan menghapusnya dari pesanan. Item dihapus saat metode deleteLineItem dipanggil. Di dalamnya metode deleteLineItem dari LineItemService menerima id item baris yang akan dihapus. Jika penghapusan berhasil, kami memperbarui jumlah item di keranjang menggunakan CartService .

Pengguna kemudian diarahkan ke halaman pelanggan di mana mereka memulai proses check out. Metode checkout melakukan perutean.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-summary', templateUrl: './summary.component.html', styleUrls: ['./summary.component.css'] }) export class SummaryComponent implements OnInit { order: Order = {}; summary: { name: string, amount: string | undefined, id: string }[] = []; constructor( private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar, private router: Router ) { } ngOnInit() { this.orders.getOrder(this.cart.orderId, GetOrderParams.cart) .subscribe( order => this.processOrder(order), err => this.showOrderError('retrieving your cart') ); } private processOrder(order: Order) { this.order = order; this.summary = [ { name: 'Subtotal', amount: order.formattedSubtotalAmount, id: 'subtotal' }, { name: 'Discount', amount: order.formattedDiscountAmount, id: 'discount' }, { name: 'Taxes (included)', amount: order.formattedTotalTaxAmount, id: 'taxes' }, { name: 'Shipping', amount: order.formattedShippingAmount, id: 'shipping' }, { name: 'Gift Card', amount: order.formattedGiftCardAmount, id: 'gift-card' } ]; } private showOrderError(msg: string) { this.snackBar.open(`There was a problem ${msg}.`, 'Close', { duration: 8000 }); } checkout() { this.router.navigateByUrl('/customer'); } deleteLineItem(id: string) { this.lineItems.deleteLineItem(id) .pipe( mergeMap(() => this.orders.getOrder(this.cart.orderId, GetOrderParams.cart)) ).subscribe( order => { this.processOrder(order); this.cart.itemCount = order.skusCount || this.cart.itemCount; this.snackBar.open(`Item successfully removed from cart.`, 'Close', { duration: 8000 }) }, err => this.showOrderError('deleting your order') ); } }

Di bawah ini adalah template dan gayanya ditautkan di sini.

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

Berikut adalah screenshot halaman tersebut.

Tangkapan layar halaman ringkasan
Tangkapan layar halaman ringkasan. (Pratinjau besar)

Modul Pembayaran

Modul ini bertanggung jawab atas proses checkout. Checkout melibatkan penyediaan alamat penagihan dan pengiriman, email pelanggan, dan memilih metode pengiriman dan pembayaran. Langkah terakhir dari proses ini adalah penempatan dan konfirmasi pesanan. Struktur modulnya adalah sebagai berikut.

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

Modul ini adalah yang terbesar sejauh ini dan berisi 3 komponen dan 7 halaman. Untuk menghasilkannya dan menjalankan komponennya:

 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

Ini adalah file modul.

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

Komponen

Komponen Pilih Negara

Komponen ini memungkinkan pengguna memilih negara sebagai bagian dari alamat. Komponen pemilihan material memiliki tampilan yang cukup berbeda jika dibandingkan dengan kolom input pada form alamat. Jadi demi keseragaman, komponen menu material digunakan sebagai gantinya.

Saat komponen diinisialisasi, data kode negara diambil menggunakan CountryService . Properti countries menyimpan nilai yang dikembalikan oleh layanan. Nilai-nilai ini akan ditambahkan ke menu di template.

Komponen memiliki satu properti keluaran, setCountryEvent . Saat sebuah negara dipilih, peristiwa ini memancarkan kode alfa-2 negara tersebut.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-country-select', templateUrl: './country-select.component.html', styleUrls: ['./country-select.component.css'] }) export class CountrySelectComponent implements OnInit { country: string = 'Country'; countries: Country[] = []; @Output() setCountryEvent = new EventEmitter<string>(); constructor(private countries: CountryService) { } ngOnInit() { this.countries.getCountries() .subscribe( countries => { this.countries = countries; } ); } setCountry(value: Country) { this.country = value.name || ''; this.setCountryEvent.emit(value.code); }}

Di bawah ini adalah templatnya dan yang ditautkan di sini adalah gayanya.

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

Komponen Alamat

Ini adalah formulir untuk menangkap alamat. Ini digunakan oleh halaman alamat pengiriman dan penagihan. Alamat Commerce Layer yang valid harus berisi nama depan dan belakang, baris alamat, kota, kode pos, kode negara bagian, kode negara, dan nomor telepon.

Layanan FormBuilder akan membuat grup formulir. Karena komponen ini digunakan oleh banyak halaman, komponen ini memiliki sejumlah properti input dan output. Properti input termasuk teks tombol, judul yang ditampilkan, dan teks untuk kotak centang. Properti keluaran akan menjadi penghasil peristiwa ketika tombol diklik untuk membuat alamat dan lainnya ketika nilai kotak centang berubah.

Ketika tombol diklik, metode addAddress dipanggil dan event createAddress memancarkan alamat lengkap. Demikian pula, ketika kotak centang dicentang, acara isCheckboxChecked memancarkan nilai kotak centang.

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

Ini dia templatenya. Anda dapat menemukan gayanya di sini.

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

halaman

Komponen Pelanggan

Pesanan harus dikaitkan dengan alamat email. Komponen ini adalah formulir yang menangkap alamat email pelanggan. Saat komponen diinisialisasi, alamat email pelanggan saat ini diambil jika mereka masuk. Kami mendapatkan pelanggan dari CustomerService . Jika mereka tidak ingin mengubah alamat email mereka, email ini akan menjadi nilai default.

Jika email diubah atau pelanggan tidak login, pesanan diperbarui dengan email yang dimasukkan. Kami menggunakan OrderService untuk memperbarui pesanan dengan alamat email baru. Jika berhasil, kami mengarahkan pelanggan ke halaman alamat penagihan.

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

Berikut adalah template komponen dan ditautkan di sini adalah gayanya.

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

Berikut adalah screenshot dari halaman pelanggan.

Tangkapan layar halaman pelanggan
Tangkapan layar halaman pelanggan. (Pratinjau besar)

Komponen Alamat Penagihan

Komponen alamat penagihan memungkinkan pelanggan menambahkan alamat penagihan baru atau memilih dari alamat yang ada. Pengguna yang tidak masuk harus memasukkan alamat baru. Mereka yang telah masuk mendapatkan opsi untuk memilih antara alamat baru atau yang sudah ada.

Properti showAddress menunjukkan apakah alamat yang ada harus ditampilkan pada komponen. sameShippingAddressAsBilling menunjukkan apakah alamat pengiriman harus sama dengan alamat penagihan yang ditetapkan. Saat pelanggan memilih alamat yang ada, maka idnya ditetapkan ke selectedCustomerAddressId .

Saat komponen diinisialisasi, kami menggunakan SessionService untuk memeriksa apakah pengguna saat ini login. Jika mereka login, kami akan menampilkan alamat mereka yang ada jika mereka memilikinya.

Seperti yang disebutkan sebelumnya, jika pengguna masuk, mereka dapat memilih alamat yang ada sebagai alamat penagihan. Dalam metode updateBillingAddress , jika mereka login, alamat yang mereka pilih akan dikloning dan ditetapkan sebagai alamat penagihan pesanan. Kami melakukan ini dengan memperbarui pesanan menggunakan metode updateOrder dari OrderService dan memberikan ID alamat.

Jika mereka tidak masuk, pengguna harus memberikan alamat. Setelah diberikan, alamat dibuat menggunakan metode createAddress . Di dalamnya, AddressService mengambil input dan membuat alamat baru. Setelah itu, pesanan diperbarui menggunakan id dari alamat yang baru dibuat. Jika ada kesalahan atau salah satu operasi berhasil, kami menampilkan snackbar.

Jika alamat yang sama dipilih sebagai alamat pengiriman, pengguna akan diarahkan ke halaman metode pengiriman. Jika mereka ingin memberikan alamat pengiriman alternatif, mereka akan diarahkan ke halaman alamat pengiriman.

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

Berikut adalah templatenya. Tautan ini menunjukkan gayanya.

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

Seperti inilah tampilan halaman alamat penagihan.

Tangkapan layar halaman alamat penagihan
Tangkapan layar halaman alamat penagihan. (Pratinjau besar)

Komponen Alamat Pengiriman

Komponen alamat pengiriman sangat mirip dengan komponen alamat penagihan. Namun, ada beberapa perbedaan. Pertama, teks yang ditampilkan pada template berbeda. Perbedaan utama lainnya adalah bagaimana pesanan diperbarui menggunakan OrderService setelah alamat dibuat atau dipilih. Bidang yang diperbarui pesanan adalah shippingAddressCloneId untuk alamat yang dipilih dan shippingAddress untuk alamat baru. Jika pengguna memilih untuk mengubah alamat penagihan, agar sama dengan alamat pengiriman, bidang billingAddressSameAsShipping akan diperbarui.

Setelah alamat pengiriman dipilih dan pesanan diperbarui, pengguna diarahkan ke halaman metode pengiriman.

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

Berikut adalah template dan gayanya dapat ditemukan di sini.

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

Halaman alamat pengiriman akan terlihat seperti ini.

Tangkapan layar halaman alamat pengiriman
Tangkapan layar halaman alamat pengiriman. (Pratinjau besar)

Komponen Metode Pengiriman

Komponen ini menampilkan jumlah pengiriman yang diperlukan untuk memenuhi pesanan, metode pengiriman yang tersedia, dan biaya terkait. Pelanggan kemudian dapat memilih metode pengiriman yang mereka inginkan untuk setiap pengiriman.

Properti shipments berisi semua pengiriman pesanan. The shipmentsForm adalah formulir di mana pemilihan metode pengiriman akan dilakukan.

Saat komponen diinisialisasi, pesanan diambil dan akan berisi item baris dan pengirimannya. Pada saat yang sama, kami mendapatkan waktu tunggu pengiriman untuk berbagai metode pengiriman. Kami menggunakan OrderService untuk mendapatkan pesanan dan DeliveryLeadTimeService untuk waktu tunggu. Setelah kedua set informasi dikembalikan, mereka digabungkan ke dalam array pengiriman dan ditugaskan ke properti shipments . Setiap pengiriman akan berisi itemnya, metode pengiriman yang tersedia, dan biaya yang sesuai.

Setelah pengguna memilih metode pengiriman untuk setiap pengiriman, metode pengiriman yang dipilih diperbarui untuk masing-masing di setShipmentMethods . Jika berhasil, pengguna diarahkan ke halaman pembayaran.

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

Berikut adalah template dan Anda dapat menemukan styling di link ini.

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

Ini adalah screenshot halaman metode pengiriman.

Tangkapan layar halaman metode pengiriman
Tangkapan layar halaman metode pengiriman. (Pratinjau besar)

Komponen Pembayaran

Pada komponen ini, pengguna mengklik tombol pembayaran jika mereka ingin melanjutkan pembayaran pesanan mereka dengan Paypal. approvalUrl adalah tautan Paypal yang diarahkan pengguna ketika mereka mengklik tombol.

Selama inisialisasi, kami mendapatkan pesanan dengan sumber pembayaran yang disertakan menggunakan OrderService . Jika sumber pembayaran disetel, kami mendapatkan id-nya dan mengambil pembayaran Paypal yang sesuai dari PaypalPaymentService . Pembayaran Paypal akan berisi url persetujuan. Jika tidak ada sumber pembayaran yang ditetapkan, kami memperbarui pesanan dengan Paypal sebagai metode pembayaran pilihan. Kami kemudian melanjutkan untuk membuat pembayaran Paypal baru untuk pesanan menggunakan PaypalPaymentService . Dari sini, kita bisa mendapatkan url persetujuan dari pesanan yang baru dibuat.

Terakhir, ketika pengguna mengklik tombol, mereka diarahkan ke Paypal di mana mereka dapat menyetujui pembelian.

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

Ini dia templatenya.

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

Berikut tampilan halaman pembayaran.

Tangkapan layar halaman pembayaran
Tangkapan layar halaman pembayaran. (Pratinjau besar)

Batalkan Komponen Pembayaran

Paypal membutuhkan halaman pembatalan pembayaran. Komponen ini melayani tujuan ini. Ini dia templatenya.

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

Berikut screenshot halaman tersebut.

Tangkapan layar halaman pembatalan pembayaran
Tangkapan layar halaman pembatalan pembayaran. (Pratinjau besar)

Tempatkan Komponen Pesanan

Ini adalah langkah terakhir dalam proses checkout. Di sini pengguna mengonfirmasi bahwa mereka memang ingin melakukan pemesanan dan memulai pemrosesannya. Ketika pengguna menyetujui pembayaran Paypal, ini adalah halaman mereka diarahkan. Paypal menambahkan parameter kueri id pembayar ke url. Ini adalah Id Paypal pengguna.

Saat komponen diinisialisasi, kami mendapatkan parameter kueri payerId dari url. Pesanan kemudian diambil menggunakan OrderService dengan menyertakan sumber pembayaran. Id dari sumber pembayaran yang disertakan digunakan untuk memperbarui pembayaran Paypal dengan id pembayar, menggunakan layanan PaypalPayment . Jika salah satu gagal, pengguna akan diarahkan ke halaman kesalahan. Kami menggunakan properti disableButton untuk mencegah pengguna melakukan pemesanan hingga ID pembayar ditetapkan.

Ketika mereka mengklik tombol pesan tempat, pesanan diperbarui dengan status placed . Setelah gerobak dibersihkan, snack bar yang sukses ditampilkan, dan pengguna diarahkan ke halaman rumah.

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

Berikut adalah template dan gaya terkaitnya.

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

Berikut adalah screenshot halaman tersebut.

Tangkapan layar halaman penempatan pesanan
Tangkapan layar halaman penempatan pesanan. (Pratinjau besar)

Modul Aplikasi

Semua permintaan yang dibuat ke Commerce Layer, selain untuk otentikasi, harus berisi token. Jadi saat aplikasi diinisialisasi, token diambil dari rute /oauth/token di server dan sesi diinisialisasi. Kami akan menggunakan token APP_INITIALIZER untuk menyediakan fungsi inisialisasi di mana token diambil. Selain itu, kami akan menggunakan token HTTP_INTERCEPTORS untuk menyediakan OptionsInterceptor yang kami buat sebelumnya. Setelah semua modul ditambahkan, file modul aplikasi akan terlihat seperti ini.

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

Komponen Aplikasi

Kami akan memodifikasi template komponen aplikasi dan gayanya yang dapat Anda temukan di sini.

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

Kesimpulan

Dalam artikel ini, kami telah membahas bagaimana Anda dapat membuat aplikasi Angular 11 e-niaga dengan Commerce Layer dan Paypal. Kami juga telah membahas cara menyusun aplikasi dan bagaimana Anda dapat berinteraksi dengan API e-niaga.

Meskipun aplikasi ini memungkinkan pelanggan untuk membuat pesanan lengkap, itu tidak berarti selesai. Ada begitu banyak yang bisa Anda tambahkan untuk memperbaikinya. Pertama, Anda dapat memilih untuk mengaktifkan perubahan jumlah item di keranjang, menautkan item keranjang ke halaman produk mereka, mengoptimalkan komponen alamat, menambahkan penjaga tambahan untuk halaman checkout seperti halaman pemesanan tempat, dan seterusnya. Ini hanya titik awal.

Jika Anda ingin memahami lebih lanjut tentang proses pembuatan pesanan dari awal hingga akhir, Anda dapat melihat panduan Commerce Layer dan API. Anda dapat melihat kode untuk proyek ini di repositori ini.