diff --git a/public/assets/images/mastercard-modern-design-.svg b/public/assets/images/mastercard-modern-design-.svg new file mode 100644 index 0000000..e2c2629 --- /dev/null +++ b/public/assets/images/mastercard-modern-design-.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/stripe-4.svg b/public/assets/images/stripe-4.svg new file mode 100644 index 0000000..ad3787f --- /dev/null +++ b/public/assets/images/stripe-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/visa-10.svg b/public/assets/images/visa-10.svg new file mode 100644 index 0000000..5e53d92 --- /dev/null +++ b/public/assets/images/visa-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/core/services/session-storage.service.ts b/src/app/core/services/session-storage.service.ts new file mode 100644 index 0000000..8907994 --- /dev/null +++ b/src/app/core/services/session-storage.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from "@angular/core"; + +@Injectable({ + providedIn: "root", +}) +export class SessionStorageService { + setItem(key: string, value: T) { + try { + const item = JSON.stringify(value); + sessionStorage.setItem(key, item); + } catch (e) { + console.error("Could not set item", e); + } + } + + getItem(key: string): T | null { + try { + const item = sessionStorage.getItem(key); + return item ? (JSON.parse(item) as T) : null; + } catch (e) { + console.error("Could not get item", e); + return null; + } + } + + /** + * @throws Error if key is not found. + * @param key + */ + removeItem(key: string): void { + sessionStorage.removeItem(key); + } + + clear(): void { + sessionStorage.clear(); + } +} diff --git a/src/app/core/services/session-storage.spec.ts b/src/app/core/services/session-storage.spec.ts new file mode 100644 index 0000000..6010dac --- /dev/null +++ b/src/app/core/services/session-storage.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { SessionStorage } from "./session-storage.service"; + +describe("SessionStorage", () => { + let service: SessionStorage; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionStorage); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/checkout.html b/src/app/features/checkout/checkout.html index 4896f21..efcb84b 100644 --- a/src/app/features/checkout/checkout.html +++ b/src/app/features/checkout/checkout.html @@ -3,7 +3,7 @@
- @if (currentStepNumber() > 0) { + @if (currentStepNumber() > 0 && currentStepNumber() < 3) { } -
+
+ @if (currentStepNumber() < 3) {
+ }
diff --git a/src/app/features/checkout/checkout.routes.ts b/src/app/features/checkout/checkout.routes.ts index 4e28c7e..35e4410 100644 --- a/src/app/features/checkout/checkout.routes.ts +++ b/src/app/features/checkout/checkout.routes.ts @@ -2,6 +2,7 @@ import { Routes } from "@angular/router"; import { Address } from "./address/address"; import { Checkout } from "./checkout"; import { Payment } from "@app/features/checkout/payment/payment"; +import Confirmation from "@app/features/checkout/confirmation/confirmation"; export const checkoutRoutes: Routes = [ { @@ -16,6 +17,10 @@ export const checkoutRoutes: Routes = [ path: "payment", component: Payment, }, + { + path: "confirmation", + component: Confirmation, + }, ], }, ]; diff --git a/src/app/features/checkout/checkout.ts b/src/app/features/checkout/checkout.ts index 872bd9a..638ca85 100644 --- a/src/app/features/checkout/checkout.ts +++ b/src/app/features/checkout/checkout.ts @@ -5,7 +5,7 @@ import { OrderSummery } from "@app/features/checkout/components/order-summery/or import { GoBack } from "@shared/components/go-back/go-back"; import { AddressService } from "@app/features/checkout/services/address-service"; import { LoadingSpinner } from "@shared/components/loading-spinner/loading-spinner"; -import { OrderService } from "@app/features/checkout/services/order-service"; +import { CheckoutResponse, OrderService } from "@app/features/checkout/services/order-service"; import { CartService } from "@core/services/cart-service"; import { CartModel } from "@core/models/cart.model"; import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; @@ -23,17 +23,21 @@ export class Checkout implements OnInit { { label: "Cart", route: "" }, { label: "Address", route: "/checkout/address" }, { label: "Payment", route: "/checkout/payment" }, - { label: "Confirm", route: "/checkout/address" }, + { label: "Confirmation", route: "/checkout/confirmation" }, ]; destroyRef = inject(DestroyRef); orderCreationLoading = signal(false); + proceedToPaymentLoading = signal(false); + private addressService = inject(AddressService); addressIdControl = this.addressService.addressIdControl; + private orderService = inject(OrderService); + paymentMethodControl = this.orderService.paymentMethodForm; + private cartService = inject(CartService); private cart: CartModel | undefined; private router = inject(Router); - protected currentStepNumber = toSignal( this.router.events.pipe( filter((event): event is NavigationEnd => event instanceof NavigationEnd), // Added TS type guard @@ -74,4 +78,32 @@ export class Checkout implements OnInit { await this.router.navigate(["/checkout/payment"]); }); } + + protected proceedToCheckout() { + if (this.paymentMethodControl.invalid) { + this.paymentMethodControl.markAsTouched(); + return; + } + this.proceedToPaymentLoading.set(true); + this.orderService + .checkout(this.paymentMethodControl.value!) + .pipe( + takeUntilDestroyed(this.destroyRef), + finalize(() => this.proceedToPaymentLoading.set(false)), + ) + .subscribe((response) => this.handleCheckout(response)); + } + private handleCheckout(data: CheckoutResponse): void { + if (data.success) { + if (data.method === "stripeCheckout") { + this.handleStripeCheckout(data); + } + } + } + private handleStripeCheckout(data: CheckoutResponse) { + console.log(data); + if (data.redirectUrl) { + window.location.href = data.redirectUrl; + } + } } diff --git a/src/app/features/checkout/components/payment-method-card/payment-method-card.css b/src/app/features/checkout/components/payment-method-card/payment-method-card.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/checkout/components/payment-method-card/payment-method-card.html b/src/app/features/checkout/components/payment-method-card/payment-method-card.html new file mode 100644 index 0000000..497ba7f --- /dev/null +++ b/src/app/features/checkout/components/payment-method-card/payment-method-card.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/features/checkout/components/payment-method-card/payment-method-card.spec.ts b/src/app/features/checkout/components/payment-method-card/payment-method-card.spec.ts new file mode 100644 index 0000000..8c4484b --- /dev/null +++ b/src/app/features/checkout/components/payment-method-card/payment-method-card.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { PaymentMethodCard } from "./payment-method-card"; + +describe("PaymentMethodCard", () => { + let component: PaymentMethodCard; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaymentMethodCard], + }).compileComponents(); + + fixture = TestBed.createComponent(PaymentMethodCard); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/components/payment-method-card/payment-method-card.ts b/src/app/features/checkout/components/payment-method-card/payment-method-card.ts new file mode 100644 index 0000000..0e71055 --- /dev/null +++ b/src/app/features/checkout/components/payment-method-card/payment-method-card.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-payment-method-card", + imports: [], + templateUrl: "./payment-method-card.html", + styleUrl: "./payment-method-card.css", +}) +export class PaymentMethodCard {} diff --git a/src/app/features/checkout/confirmation/confirmation.css b/src/app/features/checkout/confirmation/confirmation.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/checkout/confirmation/confirmation.html b/src/app/features/checkout/confirmation/confirmation.html new file mode 100644 index 0000000..a674b5a --- /dev/null +++ b/src/app/features/checkout/confirmation/confirmation.html @@ -0,0 +1,27 @@ +
+
+
+ +
+

Payment Successful

+

+ Your payment processed successfully. You will receive a confirmation email shortly. +

+
+
+

Amount

+

Rs. 20000

+
+ +
+

Transaction ID

+

text_ch_989789y789jhhg8h8

+
+
+

Payment method

+

Stripe Checkout

+
+
+
+ +
diff --git a/src/app/features/checkout/confirmation/confirmation.spec.ts b/src/app/features/checkout/confirmation/confirmation.spec.ts new file mode 100644 index 0000000..300fde9 --- /dev/null +++ b/src/app/features/checkout/confirmation/confirmation.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import Confirmation from "./confirmation"; + +describe("Confirmation", () => { + let component: Confirmation; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Confirmation], + }).compileComponents(); + + fixture = TestBed.createComponent(Confirmation); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/confirmation/confirmation.ts b/src/app/features/checkout/confirmation/confirmation.ts new file mode 100644 index 0000000..c40bc1b --- /dev/null +++ b/src/app/features/checkout/confirmation/confirmation.ts @@ -0,0 +1,24 @@ +import { Component, inject } from "@angular/core"; +import { BadgeCheck, LucideAngularModule } from "lucide-angular"; +import { GoBack } from "@shared/components/go-back/go-back"; +import { ActivatedRoute } from "@angular/router"; + +@Component({ + selector: "app-confirmation", + imports: [LucideAngularModule, GoBack], + templateUrl: "./confirmation.html", + styleUrl: "./confirmation.css", +}) +class Confirmation { + protected readonly BadgeCheck = BadgeCheck; + private route = inject(ActivatedRoute); + private orderId: string | null = null; + private sessionId: string | null = null; + + ngOnInit() { + this.orderId = this.route.snapshot.paramMap.get("order_id"); + this.sessionId = this.route.snapshot.paramMap.get("session_id"); + } +} + +export default Confirmation; diff --git a/src/app/features/checkout/payment/payment.html b/src/app/features/checkout/payment/payment.html index d9cf5ff..5e1234a 100644 --- a/src/app/features/checkout/payment/payment.html +++ b/src/app/features/checkout/payment/payment.html @@ -1 +1,51 @@ -

payment works!

+
+ +
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+

Pay using card via stripe checkout.

+
+
+
+ +
+ +
+
+
+ +
+
+

Cash on Delivery

+
+
+

Additional charges may apply.

+
+
+
+
diff --git a/src/app/features/checkout/payment/payment.ts b/src/app/features/checkout/payment/payment.ts index 6f8e9a7..3b1cde2 100644 --- a/src/app/features/checkout/payment/payment.ts +++ b/src/app/features/checkout/payment/payment.ts @@ -1,9 +1,28 @@ -import { Component } from "@angular/core"; +import { Component, inject, OnInit } from "@angular/core"; +import { PaymentMethodCard } from "@app/features/checkout/components/payment-method-card/payment-method-card"; +import { Coins, LucideAngularModule } from "lucide-angular"; +import { ReactiveFormsModule } from "@angular/forms"; +import { OrderService } from "@app/features/checkout/services/order-service"; +import { ActivatedRoute } from "@angular/router"; @Component({ selector: "app-payment", - imports: [], + imports: [PaymentMethodCard, LucideAngularModule, ReactiveFormsModule], templateUrl: "./payment.html", styleUrl: "./payment.css", }) -export class Payment {} +export class Payment implements OnInit { + coinsIcon = Coins; + private orderService = inject(OrderService); + protected paymentMethodForm = this.orderService.paymentMethodForm; + private route = inject(ActivatedRoute); + private orderId: string | null = null; + + ngOnInit() { + this.orderId = this.route.snapshot.paramMap.get("order_id"); + } + + onPaymentMethodSelected(paymentMethod: string) { + this.paymentMethodForm.setValue(paymentMethod); + } +} diff --git a/src/app/features/checkout/services/order-service.ts b/src/app/features/checkout/services/order-service.ts index 0503fe6..8e8084f 100644 --- a/src/app/features/checkout/services/order-service.ts +++ b/src/app/features/checkout/services/order-service.ts @@ -2,22 +2,63 @@ import { inject, Injectable } from "@angular/core"; import { API_URL } from "@core/tokens/api-url-tokens"; import { HttpClient } from "@angular/common/http"; import { AuthService } from "@core/services/auth-service"; +import { FormControl, Validators } from "@angular/forms"; +import { tap } from "rxjs"; +import { SessionStorageService } from "@core/services/session-storage.service"; +export interface CheckoutResponse { + success: boolean; + amount: number; + currency: string; + method: string; + redirectUrl: string | null; + errorMessage: string | null; +} export interface OrderRequest { addressId: number; cartId: number; } +export interface OrderResponse { + orderId: number; + message: string; +} @Injectable({ providedIn: "root", }) export class OrderService { - http = inject(HttpClient); - apiUrl = inject(API_URL); + public paymentMethodForm = new FormControl(null, Validators.required); + private currentOrderId: number | null = null; + private http = inject(HttpClient); + private apiUrl = inject(API_URL); private authService = inject(AuthService); private user = this.authService.user; + private sessionStorage = inject(SessionStorageService); + + constructor() { + const cachedOrderId = this.sessionStorage.getItem("orderId"); + if (cachedOrderId) { + this.currentOrderId = cachedOrderId; + } + } createOrder(data: OrderRequest) { - return this.http.post(`${this.apiUrl}/users/${this.user()?.id}/orders`, data); + return this.http + .post(`${this.apiUrl}/users/${this.user()?.id}/orders`, data) + .pipe( + tap((response) => { + this.currentOrderId = response.orderId; + this.sessionStorage.setItem("orderId", response.orderId); + }), + ); + } + + checkout(mode: string) { + return this.http.post( + `${this.apiUrl}/orders/${this.currentOrderId}/payments`, + { + mode: mode, + }, + ); } } diff --git a/src/app/shared/components/go-back/go-back.html b/src/app/shared/components/go-back/go-back.html index 4522e3f..f6819fc 100644 --- a/src/app/shared/components/go-back/go-back.html +++ b/src/app/shared/components/go-back/go-back.html @@ -1,4 +1,4 @@ - +

{{ text }}

diff --git a/src/app/shared/components/stepper/stepper.html b/src/app/shared/components/stepper/stepper.html index 2ac2146..3d06a73 100644 --- a/src/app/shared/components/stepper/stepper.html +++ b/src/app/shared/components/stepper/stepper.html @@ -3,14 +3,14 @@