Cara Membangun Situs E-Commerce Dengan Angular 11, Commerce Layer Dan Paypal
Diterbitkan: 2022-03-10Saat 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.
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.
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 .
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.
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 .
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:
- modul inti,
- modul datanya,
- modul bersama,
- modul fitur.
Modul fitur akan mengelompokkan halaman dan komponen terkait bersama-sama. Akan ada empat modul fitur:
- modul otentikasi,
- modul produk,
- modul gerobak,
- 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.
Not Found Component
This is a 404 page that the user gets redirected to when they request a route not available on the router. Only its template is modified.
<app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>
Header Component
The HeaderComponent is basically the header displayed at the top of a page. It will contain the app title, the cart, login, and logout buttons.
When this component is initialized, a request is made to check whether the user has a current session. This happens when subscribing to this.session.isCustomerLoggedIn()
. We subscribe to this.session.loggedInStatus
to check if the user logs out throughout the life of the app. The this.header.showHeaderButtons
subscription decides whether to show all the buttons on the header or hide them. this.cart.cartValue$
gets the count of items in the cart.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.